Skip to content

Commit 7b1dbb4

Browse files
authored
Merge pull request #4337 from max-schaefer/js/fix-indirect-command-injection
Approved by asgerf
2 parents 1931693 + dc7b447 commit 7b1dbb4

File tree

5 files changed

+105
-14
lines changed

5 files changed

+105
-14
lines changed

javascript/ql/src/semmle/javascript/security/dataflow/IndirectCommandArgument.qll

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ private DataFlow::Node commandArgument(SystemCommandExecution sys, DataFlow::Typ
3030
t.start() and
3131
result = sys.getACommandArgument()
3232
or
33-
exists(DataFlow::TypeBackTracker t2 | t = t2.smallstep(result, commandArgument(sys, t2)))
33+
exists(DataFlow::TypeBackTracker t2 | t2 = t.smallstep(result, commandArgument(sys, t2)))
3434
}
3535

3636
/**
@@ -63,6 +63,26 @@ private DataFlow::SourceNode argumentList(SystemCommandExecution sys) {
6363
result = argumentList(sys, DataFlow::TypeBackTracker::end())
6464
}
6565

66+
/**
67+
* Gets a data-flow node whose value ends up being interpreted as an element of the argument list
68+
* `args` after a flow summarised by `t`.
69+
*/
70+
private DataFlow::Node argumentListElement(DataFlow::SourceNode args, DataFlow::TypeBackTracker t) {
71+
t.start() and
72+
args = argumentList(_) and
73+
result = args.getAPropertyWrite().getRhs()
74+
or
75+
exists(DataFlow::TypeBackTracker t2 | t2 = t.smallstep(result, argumentListElement(args, t2)))
76+
}
77+
78+
/**
79+
* Gets a data-flow node whose value ends up being interpreted as an element of the argument list
80+
* `args`.
81+
*/
82+
private DataFlow::Node argumentListElement(DataFlow::SourceNode args) {
83+
result = argumentListElement(args, DataFlow::TypeBackTracker::end())
84+
}
85+
6686
/**
6787
* Holds if `source` contributes to the arguments of an indirect command execution `sys`.
6888
*
@@ -86,12 +106,12 @@ predicate isIndirectCommandArgument(DataFlow::Node source, SystemCommandExecutio
86106
exists(DataFlow::ArrayCreationNode args, DataFlow::Node shell, string dashC |
87107
shellCmd(shell.asExpr(), dashC) and
88108
shell = commandArgument(sys) and
89-
args.getAPropertyWrite().getRhs().mayHaveStringValue(dashC) and
90109
args = argumentList(sys) and
91-
(
92-
source = argumentList(sys)
93-
or
94-
source = argumentList(sys).getAPropertyWrite().getRhs()
110+
argumentListElement(args).mayHaveStringValue(dashC) and
111+
exists(DataFlow::SourceNode argsSource | argsSource = argumentList(sys) |
112+
if exists(argsSource.getAPropertyWrite())
113+
then source = argsSource.getAPropertyWrite().getRhs()
114+
else source = argsSource
95115
)
96116
)
97117
}

javascript/ql/test/query-tests/Security/CWE-078/CommandInjection.expected

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ nodes
2323
| child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" |
2424
| child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" |
2525
| child_process-test.js:25:21:25:23 | cmd |
26-
| child_process-test.js:39:18:39:30 | [ flag, cmd ] |
27-
| child_process-test.js:39:18:39:30 | [ flag, cmd ] |
2826
| child_process-test.js:39:26:39:28 | cmd |
2927
| child_process-test.js:39:26:39:28 | cmd |
3028
| child_process-test.js:43:15:43:17 | cmd |
@@ -36,7 +34,6 @@ nodes
3634
| child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) |
3735
| child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) |
3836
| child_process-test.js:56:46:56:57 | ["bar", cmd] |
39-
| child_process-test.js:56:46:56:57 | ["bar", cmd] |
4037
| child_process-test.js:56:54:56:56 | cmd |
4138
| child_process-test.js:56:54:56:56 | cmd |
4239
| child_process-test.js:57:25:57:49 | ['/C', ... at(cmd) |
@@ -55,6 +52,26 @@ nodes
5552
| child_process-test.js:83:19:83:36 | req.query.fileName |
5653
| child_process-test.js:85:37:85:54 | req.query.fileName |
5754
| child_process-test.js:85:37:85:54 | req.query.fileName |
55+
| exec-sh2.js:9:17:9:23 | command |
56+
| exec-sh2.js:10:40:10:46 | command |
57+
| exec-sh2.js:10:40:10:46 | command |
58+
| exec-sh2.js:14:9:14:49 | cmd |
59+
| exec-sh2.js:14:15:14:38 | url.par ... , true) |
60+
| exec-sh2.js:14:15:14:44 | url.par ... ).query |
61+
| exec-sh2.js:14:15:14:49 | url.par ... ry.path |
62+
| exec-sh2.js:14:25:14:31 | req.url |
63+
| exec-sh2.js:14:25:14:31 | req.url |
64+
| exec-sh2.js:15:12:15:14 | cmd |
65+
| exec-sh.js:13:17:13:23 | command |
66+
| exec-sh.js:15:44:15:50 | command |
67+
| exec-sh.js:15:44:15:50 | command |
68+
| exec-sh.js:19:9:19:49 | cmd |
69+
| exec-sh.js:19:15:19:38 | url.par ... , true) |
70+
| exec-sh.js:19:15:19:44 | url.par ... ).query |
71+
| exec-sh.js:19:15:19:49 | url.par ... ry.path |
72+
| exec-sh.js:19:25:19:31 | req.url |
73+
| exec-sh.js:19:25:19:31 | req.url |
74+
| exec-sh.js:20:12:20:14 | cmd |
5875
| execSeries.js:3:20:3:22 | arr |
5976
| execSeries.js:6:14:6:16 | arr |
6077
| execSeries.js:6:14:6:21 | arr[i++] |
@@ -156,12 +173,9 @@ edges
156173
| child_process-test.js:6:25:6:31 | req.url | child_process-test.js:6:15:6:38 | url.par ... , true) |
157174
| child_process-test.js:25:21:25:23 | cmd | child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" |
158175
| child_process-test.js:25:21:25:23 | cmd | child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" |
159-
| child_process-test.js:39:26:39:28 | cmd | child_process-test.js:39:18:39:30 | [ flag, cmd ] |
160-
| child_process-test.js:39:26:39:28 | cmd | child_process-test.js:39:18:39:30 | [ flag, cmd ] |
161176
| child_process-test.js:56:46:56:57 | ["bar", cmd] | child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) |
162177
| child_process-test.js:56:46:56:57 | ["bar", cmd] | child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) |
163178
| child_process-test.js:56:54:56:56 | cmd | child_process-test.js:56:46:56:57 | ["bar", cmd] |
164-
| child_process-test.js:56:54:56:56 | cmd | child_process-test.js:56:46:56:57 | ["bar", cmd] |
165179
| child_process-test.js:57:46:57:48 | cmd | child_process-test.js:57:25:57:49 | ['/C', ... at(cmd) |
166180
| child_process-test.js:57:46:57:48 | cmd | child_process-test.js:57:25:57:49 | ['/C', ... at(cmd) |
167181
| child_process-test.js:73:9:73:49 | cmd | child_process-test.js:75:29:75:31 | cmd |
@@ -174,6 +188,24 @@ edges
174188
| child_process-test.js:83:19:83:36 | req.query.fileName | child_process-test.js:83:19:83:36 | req.query.fileName |
175189
| child_process-test.js:85:37:85:54 | req.query.fileName | lib/subLib/index.js:7:32:7:35 | name |
176190
| child_process-test.js:85:37:85:54 | req.query.fileName | lib/subLib/index.js:7:32:7:35 | name |
191+
| exec-sh2.js:9:17:9:23 | command | exec-sh2.js:10:40:10:46 | command |
192+
| exec-sh2.js:9:17:9:23 | command | exec-sh2.js:10:40:10:46 | command |
193+
| exec-sh2.js:14:9:14:49 | cmd | exec-sh2.js:15:12:15:14 | cmd |
194+
| exec-sh2.js:14:15:14:38 | url.par ... , true) | exec-sh2.js:14:15:14:44 | url.par ... ).query |
195+
| exec-sh2.js:14:15:14:44 | url.par ... ).query | exec-sh2.js:14:15:14:49 | url.par ... ry.path |
196+
| exec-sh2.js:14:15:14:49 | url.par ... ry.path | exec-sh2.js:14:9:14:49 | cmd |
197+
| exec-sh2.js:14:25:14:31 | req.url | exec-sh2.js:14:15:14:38 | url.par ... , true) |
198+
| exec-sh2.js:14:25:14:31 | req.url | exec-sh2.js:14:15:14:38 | url.par ... , true) |
199+
| exec-sh2.js:15:12:15:14 | cmd | exec-sh2.js:9:17:9:23 | command |
200+
| exec-sh.js:13:17:13:23 | command | exec-sh.js:15:44:15:50 | command |
201+
| exec-sh.js:13:17:13:23 | command | exec-sh.js:15:44:15:50 | command |
202+
| exec-sh.js:19:9:19:49 | cmd | exec-sh.js:20:12:20:14 | cmd |
203+
| exec-sh.js:19:15:19:38 | url.par ... , true) | exec-sh.js:19:15:19:44 | url.par ... ).query |
204+
| exec-sh.js:19:15:19:44 | url.par ... ).query | exec-sh.js:19:15:19:49 | url.par ... ry.path |
205+
| exec-sh.js:19:15:19:49 | url.par ... ry.path | exec-sh.js:19:9:19:49 | cmd |
206+
| exec-sh.js:19:25:19:31 | req.url | exec-sh.js:19:15:19:38 | url.par ... , true) |
207+
| exec-sh.js:19:25:19:31 | req.url | exec-sh.js:19:15:19:38 | url.par ... , true) |
208+
| exec-sh.js:20:12:20:14 | cmd | exec-sh.js:13:17:13:23 | command |
177209
| execSeries.js:3:20:3:22 | arr | execSeries.js:6:14:6:16 | arr |
178210
| execSeries.js:6:14:6:16 | arr | execSeries.js:6:14:6:21 | arr[i++] |
179211
| execSeries.js:6:14:6:21 | arr[i++] | execSeries.js:14:24:14:30 | command |
@@ -247,19 +279,19 @@ edges
247279
| child_process-test.js:22:18:22:20 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:22:18:22:20 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
248280
| child_process-test.js:23:13:23:15 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:23:13:23:15 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
249281
| child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:25:13:25:31 | "foo" + cmd + "bar" | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
250-
| child_process-test.js:39:5:39:31 | cp.spaw ... cmd ]) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:39:18:39:30 | [ flag, cmd ] | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
251282
| child_process-test.js:39:5:39:31 | cp.spaw ... cmd ]) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:39:26:39:28 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
252283
| child_process-test.js:44:5:44:34 | cp.exec ... , args) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:43:15:43:17 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
253284
| child_process-test.js:54:5:54:39 | cp.exec ... , args) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:53:15:53:17 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
254285
| child_process-test.js:56:5:56:59 | cp.spaw ... cmd])) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
255-
| child_process-test.js:56:5:56:59 | cp.spaw ... cmd])) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:56:46:56:57 | ["bar", cmd] | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
256286
| child_process-test.js:56:5:56:59 | cp.spaw ... cmd])) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:56:54:56:56 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
257287
| child_process-test.js:57:5:57:50 | cp.spaw ... t(cmd)) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:6:15:6:49 | url.par ... ry.path | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
258288
| child_process-test.js:57:5:57:50 | cp.spaw ... t(cmd)) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:57:25:57:49 | ['/C', ... at(cmd) | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
259289
| child_process-test.js:62:5:62:39 | cp.exec ... , args) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:53:15:53:17 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
260290
| child_process-test.js:67:3:67:21 | cp.spawn(cmd, args) | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:48:15:48:17 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
261291
| child_process-test.js:75:29:75:31 | cmd | child_process-test.js:73:25:73:31 | req.url | child_process-test.js:75:29:75:31 | cmd | This command depends on $@. | child_process-test.js:73:25:73:31 | req.url | a user-provided value |
262292
| child_process-test.js:83:19:83:36 | req.query.fileName | child_process-test.js:83:19:83:36 | req.query.fileName | child_process-test.js:83:19:83:36 | req.query.fileName | This command depends on $@. | child_process-test.js:83:19:83:36 | req.query.fileName | a user-provided value |
293+
| exec-sh2.js:10:12:10:57 | cp.spaw ... ptions) | exec-sh2.js:14:25:14:31 | req.url | exec-sh2.js:10:40:10:46 | command | This command depends on $@. | exec-sh2.js:14:25:14:31 | req.url | a user-provided value |
294+
| exec-sh.js:15:12:15:61 | cp.spaw ... ptions) | exec-sh.js:19:25:19:31 | req.url | exec-sh.js:15:44:15:50 | command | This command depends on $@. | exec-sh.js:19:25:19:31 | req.url | a user-provided value |
263295
| execSeries.js:14:41:14:47 | command | execSeries.js:18:34:18:40 | req.url | execSeries.js:14:41:14:47 | command | This command depends on $@. | execSeries.js:18:34:18:40 | req.url | a user-provided value |
264296
| lib/subLib/index.js:8:10:8:25 | "rm -rf " + name | child_process-test.js:85:37:85:54 | req.query.fileName | lib/subLib/index.js:8:10:8:25 | "rm -rf " + name | This command depends on $@. | child_process-test.js:85:37:85:54 | req.query.fileName | a user-provided value |
265297
| other.js:7:33:7:35 | cmd | other.js:5:25:5:31 | req.url | other.js:7:33:7:35 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |

javascript/ql/test/query-tests/Security/CWE-078/UselessUseOfCat.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ options
9494
| child_process-test.js:56:5:56:59 | cp.spaw ... cmd])) | child_process-test.js:56:25:56:58 | ['/C', ... , cmd]) |
9595
| child_process-test.js:57:5:57:50 | cp.spaw ... t(cmd)) | child_process-test.js:57:25:57:49 | ['/C', ... at(cmd) |
9696
| child_process-test.js:67:3:67:21 | cp.spawn(cmd, args) | child_process-test.js:67:17:67:20 | args |
97+
| exec-sh2.js:10:12:10:57 | cp.spaw ... ptions) | exec-sh2.js:10:50:10:56 | options |
98+
| exec-sh.js:15:12:15:61 | cp.spaw ... ptions) | exec-sh.js:15:54:15:60 | options |
9799
| lib/lib.js:152:2:152:23 | cp.spaw ... gs, cb) | lib/lib.js:152:21:152:22 | cb |
98100
| lib/lib.js:159:2:159:23 | cp.spaw ... gs, cb) | lib/lib.js:159:21:159:22 | cb |
99101
| lib/lib.js:163:2:167:2 | cp.spaw ... t' }\\n\\t) | lib/lib.js:166:3:166:22 | { stdio: 'inherit' } |
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const cp = require('child_process'),
2+
http = require('http'),
3+
url = require('url');
4+
5+
function getShell() {
6+
if (process.platform === 'win32') {
7+
return { cmd: 'cmd', arg: '/C' }
8+
} else {
9+
return { cmd: 'sh', arg: '-c' }
10+
}
11+
}
12+
13+
function execSh(command, options) {
14+
var shell = getShell()
15+
return cp.spawn(shell.cmd, [shell.arg, command], options) // BAD
16+
}
17+
18+
http.createServer(function (req, res) {
19+
let cmd = url.parse(req.url, true).query.path;
20+
execSh(cmd);
21+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const cp = require('child_process'),
2+
http = require('http'),
3+
url = require('url');
4+
5+
function getShell() {
6+
return "sh";
7+
}
8+
9+
function execSh(command, options) {
10+
return cp.spawn(getShell(), ["-c", command], options) // BAD
11+
};
12+
13+
http.createServer(function (req, res) {
14+
let cmd = url.parse(req.url, true).query.path;
15+
execSh(cmd);
16+
});

0 commit comments

Comments
 (0)