Skip to content

Commit b38a1b3

Browse files
committed
[PLXUTILS-161] Commandline shell injection problems
Patch by Charles Duffy, applied unmodified
1 parent 86c3798 commit b38a1b3

File tree

5 files changed

+107
-82
lines changed

5 files changed

+107
-82
lines changed

src/main/java/org/codehaus/plexus/util/cli/Commandline.java

+30-8
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public class Commandline
139139
* Create a new command line object.
140140
* Shell is autodetected from operating system
141141
*
142+
* Shell usage is only desirable when generating code for remote execution.
143+
*
142144
* @param toProcess
143145
*/
144146
public Commandline( String toProcess, Shell shell )
@@ -167,15 +169,16 @@ public Commandline( String toProcess, Shell shell )
167169
/**
168170
* Create a new command line object.
169171
* Shell is autodetected from operating system
172+
*
173+
* Shell usage is only desirable when generating code for remote execution.
170174
*/
171175
public Commandline( Shell shell )
172176
{
173177
this.shell = shell;
174178
}
175179

176180
/**
177-
* Create a new command line object.
178-
* Shell is autodetected from operating system
181+
* Create a new command line object, given a command following POSIX sh quoting rules
179182
*
180183
* @param toProcess
181184
*/
@@ -203,7 +206,6 @@ public Commandline( String toProcess )
203206

204207
/**
205208
* Create a new command line object.
206-
* Shell is autodetected from operating system
207209
*/
208210
public Commandline()
209211
{
@@ -253,7 +255,7 @@ public int getPosition()
253255
{
254256
if ( realPos == -1 )
255257
{
256-
realPos = ( getExecutable() == null ? 0 : 1 );
258+
realPos = ( getLiteralExecutable() == null ? 0 : 1 );
257259
for ( int i = 0; i < position; i++ )
258260
{
259261
Arg arg = (Arg) arguments.elementAt( i );
@@ -404,6 +406,21 @@ public void setExecutable( String executable )
404406
this.executable = executable;
405407
}
406408

409+
/**
410+
* @return Executable to be run, as a literal string (no shell quoting/munging)
411+
*/
412+
public String getLiteralExecutable()
413+
{
414+
return executable;
415+
}
416+
417+
/**
418+
* Return an executable name, quoted for shell use.
419+
*
420+
* Shell usage is only desirable when generating code for remote execution.
421+
*
422+
* @return Executable to be run, quoted for shell interpretation
423+
*/
407424
public String getExecutable()
408425
{
409426
String exec = shell.getExecutable();
@@ -483,7 +500,7 @@ public String[] getEnvironmentVariables()
483500
public String[] getCommandline()
484501
{
485502
final String[] args = getArguments();
486-
String executable = getExecutable();
503+
String executable = getLiteralExecutable();
487504

488505
if ( executable == null )
489506
{
@@ -497,6 +514,8 @@ public String[] getCommandline()
497514

498515
/**
499516
* Returns the shell, executable and all defined arguments.
517+
*
518+
* Shell usage is only desirable when generating code for remote execution.
500519
*/
501520
public String[] getShellCommandline()
502521
{
@@ -633,7 +652,7 @@ public Process execute()
633652
{
634653
if ( workingDir == null )
635654
{
636-
process = Runtime.getRuntime().exec( getShellCommandline(), environment );
655+
process = Runtime.getRuntime().exec( getCommandline(), environment, workingDir );
637656
}
638657
else
639658
{
@@ -648,7 +667,7 @@ else if ( !workingDir.isDirectory() )
648667
+ "\" does not specify a directory." );
649668
}
650669

651-
process = Runtime.getRuntime().exec( getShellCommandline(), environment, workingDir );
670+
process = Runtime.getRuntime().exec( getCommandline(), environment, workingDir );
652671
}
653672
}
654673
catch ( IOException ex )
@@ -669,7 +688,7 @@ private void verifyShellState()
669688
shell.setWorkingDirectory( workingDir );
670689
}
671690

672-
if ( shell.getExecutable() == null )
691+
if ( shell.getOriginalExecutable() == null )
673692
{
674693
shell.setExecutable( executable );
675694
}
@@ -684,6 +703,8 @@ public Properties getSystemEnvVars()
684703
/**
685704
* Allows to set the shell to be used in this command line.
686705
*
706+
* Shell usage is only desirable when generating code for remote execution.
707+
*
687708
* @param shell
688709
* @since 1.2
689710
*/
@@ -695,6 +716,7 @@ public void setShell( Shell shell )
695716
/**
696717
* Get the shell to be used in this command line.
697718
*
719+
* Shell usage is only desirable when generating code for remote execution.
698720
* @since 1.2
699721
*/
700722
public Shell getShell()

src/main/java/org/codehaus/plexus/util/cli/shell/BourneShell.java

+19-41
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818

1919
import org.codehaus.plexus.util.Os;
20-
import org.codehaus.plexus.util.StringUtils;
2120

2221
import java.util.ArrayList;
2322
import java.util.List;
@@ -29,34 +28,18 @@
2928
public class BourneShell
3029
extends Shell
3130
{
32-
private static final char[] BASH_QUOTING_TRIGGER_CHARS = {
33-
' ',
34-
'$',
35-
';',
36-
'&',
37-
'|',
38-
'<',
39-
'>',
40-
'*',
41-
'?',
42-
'(',
43-
')',
44-
'[',
45-
']',
46-
'{',
47-
'}',
48-
'`' };
4931

5032
public BourneShell()
5133
{
52-
this( false );
34+
this(false);
5335
}
5436

5537
public BourneShell( boolean isLoginShell )
5638
{
39+
setUnconditionalQuoting( true );
5740
setShellCommand( "/bin/sh" );
5841
setArgumentQuoteDelimiter( '\'' );
59-
setExecutableQuoteDelimiter( '\"' );
42+
setExecutableQuoteDelimiter( '\'' );
6043
setSingleQuotedArgumentEscaped( true );
6144
setSingleQuotedExecutableEscaped( false );
6245
setQuotedExecutableEnabled( true );
@@ -76,7 +59,7 @@ public String getExecutable()
7659
return super.getExecutable();
7760
}
7861

79-
return unifyQuotes( super.getExecutable());
62+
return quoteOneItem( super.getOriginalExecutable(), true );
8063
}
8164

8265
public List<String> getShellArgsList()
@@ -126,46 +109,41 @@ protected String getExecutionPreamble()
126109
StringBuilder sb = new StringBuilder();
127110
sb.append( "cd " );
128111

129-
sb.append( unifyQuotes( dir ) );
112+
sb.append( quoteOneItem( dir, false ) );
130113
sb.append( " && " );
131114

132115
return sb.toString();
133116
}
134117

135-
protected char[] getQuotingTriggerChars()
136-
{
137-
return BASH_QUOTING_TRIGGER_CHARS;
138-
}
139-
140118
/**
141119
* <p>Unify quotes in a path for the Bourne Shell.</p>
142120
*
143121
* <pre>
144-
* BourneShell.unifyQuotes(null) = null
145-
* BourneShell.unifyQuotes("") = (empty)
146-
* BourneShell.unifyQuotes("/test/quotedpath'abc") = /test/quotedpath\'abc
147-
* BourneShell.unifyQuotes("/test/quoted path'abc") = "/test/quoted path'abc"
148-
* BourneShell.unifyQuotes("/test/quotedpath\"abc") = "/test/quotedpath\"abc"
149-
* BourneShell.unifyQuotes("/test/quoted path\"abc") = "/test/quoted path\"abc"
150-
* BourneShell.unifyQuotes("/test/quotedpath\"'abc") = "/test/quotedpath\"'abc"
151-
* BourneShell.unifyQuotes("/test/quoted path\"'abc") = "/test/quoted path\"'abc"
122+
* BourneShell.quoteOneItem(null) = null
123+
* BourneShell.quoteOneItem("") = ''
124+
* BourneShell.quoteOneItem("/test/quotedpath'abc") = '/test/quotedpath'"'"'abc'
125+
* BourneShell.quoteOneItem("/test/quoted path'abc") = '/test/quoted pat'"'"'habc'
126+
* BourneShell.quoteOneItem("/test/quotedpath\"abc") = '/test/quotedpath"abc'
127+
* BourneShell.quoteOneItem("/test/quoted path\"abc") = '/test/quoted path"abc'
128+
* BourneShell.quoteOneItem("/test/quotedpath\"'abc") = '/test/quotedpath"'"'"'abc'
129+
* BourneShell.quoteOneItem("/test/quoted path\"'abc") = '/test/quoted path"'"'"'abc'
152130
* </pre>
153131
*
154132
* @param path not null path.
155133
* @return the path unified correctly for the Bourne shell.
156134
*/
157-
protected static String unifyQuotes( String path )
135+
protected String quoteOneItem( String path, boolean isExecutable )
158136
{
159137
if ( path == null )
160138
{
161139
return null;
162140
}
163141

164-
if ( path.indexOf( " " ) == -1 && path.indexOf( "'" ) != -1 && path.indexOf( "\"" ) == -1 )
165-
{
166-
return StringUtils.escape( path );
167-
}
142+
StringBuilder sb = new StringBuilder();
143+
sb.append( "'" );
144+
sb.append( path.replace( "'", "'\"'\"'" ) );
145+
sb.append( "'" );
168146

169-
return StringUtils.quoteAndEscape( path, '\"', BASH_QUOTING_TRIGGER_CHARS );
147+
return sb.toString();
170148
}
171149
}

src/main/java/org/codehaus/plexus/util/cli/shell/Shell.java

+28-7
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class Shell
4848

4949
private boolean quotedArgumentsEnabled = true;
5050

51+
private boolean unconditionallyQuote = false;
52+
5153
private String executable;
5254

5355
private String workingDir;
@@ -68,6 +70,16 @@ public class Shell
6870

6971
private String argumentEscapePattern = "\\%s";
7072

73+
/**
74+
* Toggle unconditional quoting
75+
*
76+
* @param unconditionallyQuote
77+
*/
78+
public void setUnconditionalQuoting(boolean unconditionallyQuote)
79+
{
80+
this.unconditionallyQuote = unconditionallyQuote;
81+
}
82+
7183
/**
7284
* Set the command to execute the shell (eg. COMMAND.COM, /bin/bash,...)
7385
*
@@ -129,6 +141,19 @@ public List<String> getCommandLine( String executable, String[] arguments )
129141
return getRawCommandLine( executable, arguments );
130142
}
131143

144+
protected String quoteOneItem(String inputString, boolean isExecutable)
145+
{
146+
char[] escapeChars = getEscapeChars( isSingleQuotedExecutableEscaped(), isDoubleQuotedExecutableEscaped() );
147+
return StringUtils.quoteAndEscape(
148+
inputString,
149+
isExecutable ? getExecutableQuoteDelimiter() : getArgumentQuoteDelimiter(),
150+
escapeChars,
151+
getQuotingTriggerChars(),
152+
'\\',
153+
unconditionallyQuote
154+
);
155+
}
156+
132157
protected List<String> getRawCommandLine( String executable, String[] arguments )
133158
{
134159
List<String> commandLine = new ArrayList<String>();
@@ -144,9 +169,7 @@ protected List<String> getRawCommandLine( String executable, String[] arguments
144169

145170
if ( isQuotedExecutableEnabled() )
146171
{
147-
char[] escapeChars = getEscapeChars( isSingleQuotedExecutableEscaped(), isDoubleQuotedExecutableEscaped() );
148-
149-
sb.append( StringUtils.quoteAndEscape( getExecutable(), getExecutableQuoteDelimiter(), escapeChars, getQuotingTriggerChars(), '\\', false ) );
172+
sb.append( quoteOneItem( getOriginalExecutable(), true ) );
150173
}
151174
else
152175
{
@@ -162,9 +185,7 @@ protected List<String> getRawCommandLine( String executable, String[] arguments
162185

163186
if ( isQuotedArgumentsEnabled() )
164187
{
165-
char[] escapeChars = getEscapeChars( isSingleQuotedArgumentEscaped(), isDoubleQuotedArgumentEscaped() );
166-
167-
sb.append( StringUtils.quoteAndEscape( arguments[i], getArgumentQuoteDelimiter(), escapeChars, getQuotingTriggerChars(), getArgumentEscapePattern(), false ) );
188+
sb.append( quoteOneItem( arguments[i], false ) );
168189
}
169190
else
170191
{
@@ -278,7 +299,7 @@ public List<String> getShellCommandLine( String[] arguments )
278299
commandLine.addAll( getShellArgsList() );
279300
}
280301

281-
commandLine.addAll( getCommandLine( getExecutable(), arguments ) );
302+
commandLine.addAll( getCommandLine( getOriginalExecutable(), arguments ) );
282303

283304
return commandLine;
284305

0 commit comments

Comments
 (0)