Skip to content

Commit 8599126

Browse files
authored
Merge pull request github#13661 from github/max-schaefer/improve-command-injection-qhelp
JavaScript: Improve query help for js/command-line-injection
2 parents a0e9659 + b8eb2ef commit 8599126

13 files changed

+166
-39
lines changed

javascript/ql/src/Security/CWE-078/CommandInjection.inc.qhelp

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,65 @@
22
"-//Semmle//qhelp//EN"
33
"qhelp.dtd">
44
<qhelp>
5-
<overview>
6-
<p>Code that passes user input directly to
7-
<code>require('child_process').exec</code>, or some other library
8-
routine that executes a command, allows the user to execute malicious
9-
code.</p>
105

6+
<overview>
7+
<p>Code that passes untrusted user input directly to
8+
<code>child_process.exec</code> or similar APIs that execute shell commands
9+
allows the user to execute malicious code.</p>
1110
</overview>
12-
<recommendation>
1311

14-
<p>If possible, use hard-coded string literals to specify the command to run
15-
or library to load. Instead of passing the user input directly to the
16-
process or library function, examine the user input and then choose
17-
among hard-coded string literals.</p>
12+
<recommendation>
13+
<p>If possible, use APIs that don't run shell commands and that accept command
14+
arguments as an array of strings rather than a single concatenated string. This
15+
is both safer and more portable.</p>
1816

19-
<p>If the applicable libraries or commands cannot be determined at
20-
compile time, then add code to verify that the user input string is
21-
safe before using it.</p>
17+
<p>If given arguments as a single string, avoid simply splitting the string on
18+
whitespace. Arguments may contain quoted whitespace, causing them to split into
19+
multiple arguments. Use a library like <code>shell-quote</code> to parse the string
20+
into an array of arguments instead.</p>
2221

22+
<p>If this approach is not viable, then add code to verify that the user input
23+
string is safe before using it.</p>
2324
</recommendation>
24-
<example>
2525

26-
<p>The following example shows code that takes a shell script that can be changed
27-
maliciously by a user, and passes it straight to <code>child_process.exec</code>
28-
without examining it first.</p>
26+
<example>
27+
<p>The following example shows code that extracts a filename from an HTTP query
28+
parameter that may contain untrusted data, and then embeds it into a shell
29+
command to count its lines without examining it first:</p>
2930

3031
<sample src="examples/command-injection.js" />
3132

33+
<p>A malicious user can take advantage of this code by executing arbitrary shell commands. For example, by providing a filename like <code>foo.txt; rm -rf .</code>, the user can first count the lines in <code>foo.txt</code> and subsequently delete all files in the current directory. </p>
34+
35+
<p>To avoid this catastrophic behavior, use an API such as
36+
<code>child_process.execFileSync</code> that does not spawn a shell by
37+
default:</p>
38+
39+
<sample src="examples/command-injection_fixed.js" />
40+
41+
<p>If you want to allow the user to specify other options to <code>wc</code>,
42+
you can use a library like <code>shell-quote</code> to parse the user input into
43+
an array of arguments without risking command injection:</p>
44+
45+
<sample src="examples/command-injection_shellquote.js" />
46+
47+
<p>Alternatively, the original example can be made safe by checking the filename
48+
against an allowlist of safe characters before using it:</p>
49+
50+
<sample src="examples/command-injection_allowlist.js" />
3251
</example>
33-
<references>
3452

53+
<references>
3554
<li>
3655
OWASP:
3756
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
3857
</li>
39-
58+
<li>
59+
npm:
60+
<a href="https://www.npmjs.com/package/shell-quote">shell-quote</a>.
61+
</li>
4062
<!-- LocalWords: CWE untrusted unsanitized Runtime
4163
-->
42-
4364
</references>
65+
4466
</qhelp>

javascript/ql/src/Security/CWE-078/IndirectCommandInjection.qhelp

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,25 @@
2727

2828
<p>
2929

30-
If possible, use hard-coded string literals to specify the
31-
command to run or library to load. Instead of forwarding the
32-
command-line arguments to the process, examine the command-line
33-
arguments and then choose among hard-coded string literals.
30+
If possible, use APIs that don't run shell commands and accept
31+
command arguments as an array of strings rather than a single
32+
concatenated string. This is both safer and more portable.
3433

3534
</p>
3635

3736
<p>
3837

39-
If the applicable libraries or commands cannot be determined
40-
at compile time, then add code to verify that each forwarded
41-
command-line argument is properly escaped before using it.
38+
If given arguments as a single string, avoid simply splitting the string on
39+
whitespace. Arguments may contain quoted whitespace, causing them to split into
40+
multiple arguments. Use a library like <code>shell-quote</code> to parse the string
41+
into an array of arguments instead.
4242

4343
</p>
4444

4545
<p>
4646

47-
If the forwarded command-line arguments are part of the
48-
arguments of the system command, prefer a library routine that handles
49-
the arguments as an array of strings rather than a single concatenated
50-
string. This prevents the unexpected evaluation of special characters.
47+
If this approach is not viable, then add code to verify that each
48+
forwarded command-line argument is properly escaped before using it.
5149

5250
</p>
5351

@@ -91,6 +89,17 @@
9189

9290
<sample src="examples/indirect-command-injection_fixed.js" />
9391

92+
<p>
93+
94+
If you want to allow the user to specify other options to
95+
<code>node</code>, you can use a library like
96+
<code>shell-quote</code> to parse the user input into an array of
97+
arguments without risking command injection:
98+
99+
</p>
100+
101+
<sample src="examples/indirect-command-injection_shellquote.js" />
102+
94103
</example>
95104

96105
<references>
@@ -100,6 +109,11 @@
100109
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
101110
</li>
102111

112+
<li>
113+
npm:
114+
<a href="https://www.npmjs.com/package/shell-quote">shell-quote</a>.
115+
</li>
116+
103117
</references>
104118

105119
</qhelp>

javascript/ql/src/Security/CWE-078/UnsafeShellCommandConstruction.qhelp

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,22 @@
2727
</p>
2828

2929
<p>
30-
Alternatively, if the shell command must be constructed
31-
dynamically, then add code to ensure that special characters
32-
do not alter the shell command unexpectedly.
30+
31+
If given arguments as a single string, avoid simply splitting the string
32+
on whitespace. Arguments may contain quoted whitespace, causing them to
33+
split into multiple arguments. Use a library like
34+
<code>shell-quote</code> to parse the string into an array of arguments
35+
instead.
36+
37+
</p>
38+
39+
<p>
40+
41+
Alternatively, if the command must be interpreted by a shell (for
42+
example because it includes I/O redirections), you can use
43+
<code>shell-quote</code> to escape any special characters in the input
44+
before embedding it in the command.
45+
3346
</p>
3447

3548
</recommendation>
@@ -63,6 +76,27 @@
6376

6477
<sample src="examples/unsafe-shell-command-construction_fixed.js" />
6578

79+
<p>
80+
81+
As another example, consider the following code which is similar to the
82+
preceding example, but pipes the output of <code>wget</code> into <code>wc -l</code>
83+
to count the number of lines in the downloaded file.
84+
85+
</p>
86+
87+
<sample src="examples/unsafe-shell-command-construction_pipe.js" />
88+
89+
<p>
90+
91+
In this case, using <code>child_process.execFile</code> is not an option
92+
because the shell is needed to interpret the pipe operator. Instead, you
93+
can use <code>shell-quote</code> to escape the input before embedding it
94+
in the command:
95+
96+
</p>
97+
98+
<sample src="examples/unsafe-shell-command-construction_pipe_fixed.js" />
99+
66100
</example>
67101
<references>
68102

@@ -71,5 +105,10 @@
71105
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
72106
</li>
73107

108+
<li>
109+
npm:
110+
<a href="https://www.npmjs.com/package/shell-quote">shell-quote</a>.
111+
</li>
112+
74113
</references>
75114
</qhelp>

javascript/ql/src/Security/CWE-078/examples/command-injection.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ var cp = require("child_process"),
33
url = require('url');
44

55
var server = http.createServer(function(req, res) {
6-
let cmd = url.parse(req.url, true).query.path;
6+
let file = url.parse(req.url, true).query.path;
77

8-
cp.exec(cmd); // BAD
8+
cp.execSync(`wc -l ${file}`); // BAD
99
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
var cp = require("child_process"),
2+
http = require('http'),
3+
url = require('url');
4+
5+
var server = http.createServer(function(req, res) {
6+
let file = url.parse(req.url, true).query.path;
7+
8+
// only allow safe characters in file name
9+
if (file.match(/^[\w\.\-\/]+$/)) {
10+
cp.execSync(`wc -l ${file}`); // GOOD
11+
}
12+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
var cp = require("child_process"),
2+
http = require('http'),
3+
url = require('url');
4+
5+
var server = http.createServer(function(req, res) {
6+
let file = url.parse(req.url, true).query.path;
7+
8+
cp.execFileSync('wc', ['-l', file]); // GOOD
9+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var cp = require("child_process"),
2+
http = require('http'),
3+
url = require('url'),
4+
shellQuote = require('shell-quote');
5+
6+
var server = http.createServer(function(req, res) {
7+
let options = url.parse(req.url, true).query.options;
8+
9+
cp.execFileSync('wc', shellQuote.parse(options)); // GOOD
10+
});

javascript/ql/src/Security/CWE-078/examples/indirect-command-injection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ var cp = require("child_process");
22

33
const args = process.argv.slice(2);
44
const script = path.join(__dirname, 'bin', 'main.js');
5-
cp.execSync(`node ${script} ${args.join(' ')}"`); // BAD
5+
cp.execSync(`node ${script} ${args.join(' ')}`); // BAD
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var cp = require("child_process"),
2+
shellQuote = require("shell-quote");
3+
4+
const args = process.argv.slice(2);
5+
let nodeOpts = '';
6+
if (args[0] === '--node-opts') {
7+
nodeOpts = args[1];
8+
args.splice(0, 2);
9+
}
10+
const script = path.join(__dirname, 'bin', 'main.js');
11+
cp.execFileSync('node', shellQuote.parse(nodeOpts).concat(script).concat(args)); // GOOD
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
var cp = require("child_process");
22

33
module.exports = function download(path, callback) {
4-
cp.execSync("wget " + path, callback);
4+
cp.exec("wget " + path, callback);
55
}

0 commit comments

Comments
 (0)