Skip to content

Commit 1c79ec5

Browse files
authored
Merge pull request #2092 from esben-semmle/js/brittle-system-reflection-command
Approved by mchammer01, xiemaisi
2 parents eb9d90d + 5a983cb commit 1c79ec5

17 files changed

+353
-24
lines changed

change-notes/1.23/analysis-javascript.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
| Unused index variable (`js/unused-index-variable`) | correctness | Highlights loops that iterate over an array, but do not use the index variable to access array elements, indicating a possible typo or logic error. Results are shown on LGTM by default. |
2020
| Loop bound injection (`js/loop-bound-injection`) | security, external/cwe/cwe-834 | Highlights loops where a user-controlled object with an arbitrary .length value can trick the server to loop indefinitely. Results are not shown on LGTM by default. |
2121
| Suspicious method name (`js/suspicious-method-name-declaration`) | correctness, typescript, methods | Highlights suspiciously named methods where the developer likely meant to write a constructor or function. Results are shown on LGTM by default. |
22+
| Shell command built from environment values (`js/shell-command-injection-from-environment`) | correctness, security, external/cwe/cwe-078, external/cwe/cwe-088 | Highlights shell commands that may change behavior inadvertently depending on the execution environment, indicating a possible violation of [CWE-78](https://cwe.mitre.org/data/definitions/78.html). Results are shown on LGTM by default.|
2223
| Use of returnless function (`js/use-of-returnless-function`) | maintainability, correctness | Highlights calls where the return value is used, but the callee never returns a value. Results are shown on LGTM by default. |
2324
| Useless regular expression character escape (`js/useless-regexp-character-escape`) | correctness, security, external/cwe/cwe-20 | Highlights regular expression strings with useless character escapes, indicating a possible violation of [CWE-20](https://cwe.mitre.org/data/definitions/20.html). Results are shown on LGTM by default. |
2425
| Unreachable method overloads (`js/unreachable-method-overloads`) | correctness, typescript | Highlights method overloads that are impossible to use from client code. Results are shown on LGTM by default. |

javascript/config/suites/javascript/security

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
+ semmlecode-javascript-queries/Security/CWE-022/ZipSlip.ql: /Security/CWE/CWE-022
1010
+ semmlecode-javascript-queries/Security/CWE-078/CommandInjection.ql: /Security/CWE/CWE-078
1111
+ semmlecode-javascript-queries/Security/CWE-078/IndirectCommandInjection.ql: /Security/CWE/CWE-078
12+
+ semmlecode-javascript-queries/Security/CWE-078/ShellCommandInjectionFromEnvironment: /Security/CWE/CWE-078
1213
+ semmlecode-javascript-queries/Security/CWE-079/ReflectedXss.ql: /Security/CWE/CWE-079
1314
+ semmlecode-javascript-queries/Security/CWE-079/StoredXss.ql: /Security/CWE/CWE-079
1415
+ semmlecode-javascript-queries/Security/CWE-079/Xss.ql: /Security/CWE/CWE-079
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>
7+
8+
Dynamically constructing a shell command with values from the
9+
local environment, such as file paths, may inadvertently
10+
change the meaning of the shell command.
11+
12+
Such changes can occur when an environment value contains
13+
characters that the shell interprets in a special way, for instance
14+
quotes and spaces.
15+
16+
This can result in the shell command misbehaving, or even
17+
allowing a malicious user to execute arbitrary commands on the system.
18+
19+
</p>
20+
21+
22+
</overview>
23+
<recommendation>
24+
25+
<p>
26+
27+
If possible, use hard-coded string literals to specify the
28+
shell command to run, and provide the dynamic arguments to the shell
29+
command separately to avoid interpretation by the shell.
30+
31+
</p>
32+
33+
<p>
34+
35+
Alternatively, if the shell command must be constructed
36+
dynamically, then add code to ensure that special characters in
37+
environment values do not alter the shell command unexpectedly.
38+
39+
</p>
40+
41+
</recommendation>
42+
<example>
43+
44+
<p>
45+
46+
The following example shows a dynamically constructed shell
47+
command that recursively removes a temporary directory that is located
48+
next to the currently executing JavaScript file. Such utilities are
49+
often found in custom build scripts.
50+
51+
</p>
52+
53+
<sample src="examples/shell-command-injection-from-environment.js" />
54+
55+
<p>
56+
57+
The shell command will, however, fail to work as intended if the
58+
absolute path of the script's directory contains spaces. In that
59+
case, the shell command will interpret the absolute path as multiple
60+
paths, instead of a single path.
61+
</p>
62+
63+
<p>
64+
65+
For instance, if the absolute path of
66+
the temporary directory is <code>/home/username/important
67+
project/temp</code>, then the shell command will recursively delete
68+
<code>/home/username/important</code> and <code>project/temp</code>,
69+
where the latter path gets resolved relative to the working directory
70+
of the JavaScript process.
71+
72+
</p>
73+
74+
75+
<p>
76+
77+
Even worse, although less likely, a malicious user could
78+
provide the path <code>/home/username/; cat /etc/passwd #/important
79+
project/temp</code> in order to execute the command <code>cat
80+
/etc/passwd</code>.
81+
82+
</p>
83+
84+
<p>
85+
86+
To avoid such potentially catastrophic behaviors, provide the
87+
directory as an argument that does not get interpreted by a
88+
shell:
89+
90+
</p>
91+
92+
<sample src="examples/shell-command-injection-from-environment_fixed.js" />
93+
94+
</example>
95+
<references>
96+
97+
<li>
98+
OWASP:
99+
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
100+
</li>
101+
102+
</references>
103+
</qhelp>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @name Shell command built from environment values
3+
* @description Building a shell command string with values from the enclosing
4+
* environment may cause subtle bugs or vulnerabilities.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id js/shell-command-injection-from-environment
9+
* @tags correctness
10+
* security
11+
* external/cwe/cwe-078
12+
* external/cwe/cwe-088
13+
*/
14+
15+
import javascript
16+
import DataFlow::PathGraph
17+
import semmle.javascript.security.dataflow.ShellCommandInjectionFromEnvironment::ShellCommandInjectionFromEnvironment
18+
19+
from
20+
Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight,
21+
Source sourceNode
22+
where
23+
sourceNode = source.getNode() and
24+
cfg.hasFlowPath(source, sink) and
25+
if cfg.isSinkWithHighlight(sink.getNode(), _)
26+
then cfg.isSinkWithHighlight(sink.getNode(), highlight)
27+
else highlight = sink.getNode()
28+
select highlight, source, sink, "This shell command depends on an uncontrolled $@.", sourceNode,
29+
sourceNode.getSourceType()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var cp = require("child_process"),
2+
path = require("path");
3+
function cleanupTemp() {
4+
let cmd = "rm -rf " + path.join(__dirname, "temp");
5+
cp.execSync(cmd); // BAD
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var cp = require("child_process"),
2+
path = require("path");
3+
function cleanupTemp() {
4+
let cmd = "rm",
5+
args = ["-rf", path.join(__dirname, "temp")];
6+
cp.execFileSync(cmd, args); // GOOD
7+
}

javascript/ql/src/semmle/javascript/Concepts.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ abstract class SystemCommandExecution extends DataFlow::Node {
1414
/** Gets an argument to this execution that specifies the command. */
1515
abstract DataFlow::Node getACommandArgument();
1616

17+
/** Holds if a shell interprets `arg`. */
18+
predicate isShellInterpreted(DataFlow::Node arg) { none() }
19+
1720
/**
1821
* Gets an argument to this command execution that specifies the argument list
1922
* to the command.

javascript/ql/src/semmle/javascript/frameworks/NodeJSLib.qll

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -573,21 +573,32 @@ module NodeJSLib {
573573
this = DataFlow::moduleMember("child_process", methodName).getACall()
574574
}
575575

576-
override DataFlow::Node getACommandArgument() {
576+
private DataFlow::Node getACommandArgument(boolean shell) {
577577
// check whether this is an invocation of an exec/spawn/fork method
578578
(
579-
methodName = "exec" or
580-
methodName = "execSync" or
581-
methodName = "execFile" or
582-
methodName = "execFileSync" or
583-
methodName = "spawn" or
584-
methodName = "spawnSync" or
585-
methodName = "fork"
579+
shell = true and
580+
(
581+
methodName = "exec" or
582+
methodName = "execSync"
583+
)
584+
or
585+
shell = false and
586+
(
587+
methodName = "execFile" or
588+
methodName = "execFileSync" or
589+
methodName = "spawn" or
590+
methodName = "spawnSync" or
591+
methodName = "fork"
592+
)
586593
) and
587594
// all of the above methods take the command as their first argument
588595
result = getArgument(0)
589596
}
590597

598+
override DataFlow::Node getACommandArgument() { result = getACommandArgument(_) }
599+
600+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument(true) }
601+
591602
override DataFlow::Node getArgumentList() {
592603
(
593604
methodName = "execFile" or

javascript/ql/src/semmle/javascript/frameworks/ShellJS.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ module ShellJS {
158158
ShellJSExec() { name = "exec" }
159159

160160
override DataFlow::Node getACommandArgument() { result = getArgument(0) }
161+
162+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
161163
}
162164

163165
/**

javascript/ql/src/semmle/javascript/frameworks/SystemCommandExecutors.qll

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,36 +8,51 @@ import javascript
88
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
99
int cmdArg;
1010

11+
boolean shell;
12+
1113
SystemCommandExecutors() {
1214
exists(string mod, DataFlow::SourceNode callee |
1315
exists(string method |
14-
mod = "cross-spawn" and method = "sync" and cmdArg = 0
16+
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false
1517
or
1618
mod = "execa" and
1719
(
18-
method = "shell" or
19-
method = "shellSync" or
20-
method = "stdout" or
21-
method = "stderr" or
22-
method = "sync"
20+
shell = false and
21+
(
22+
method = "shell" or
23+
method = "shellSync" or
24+
method = "stdout" or
25+
method = "stderr" or
26+
method = "sync"
27+
)
28+
or
29+
shell = true and
30+
(method = "command" or method = "commandSync")
2331
) and
2432
cmdArg = 0
2533
|
2634
callee = DataFlow::moduleMember(mod, method)
2735
)
2836
or
2937
(
30-
mod = "cross-spawn" and cmdArg = 0
31-
or
32-
mod = "cross-spawn-async" and cmdArg = 0
33-
or
34-
mod = "exec" and cmdArg = 0
35-
or
36-
mod = "exec-async" and cmdArg = 0
37-
or
38-
mod = "execa" and cmdArg = 0
38+
shell = false and
39+
(
40+
mod = "cross-spawn" and cmdArg = 0
41+
or
42+
mod = "cross-spawn-async" and cmdArg = 0
43+
or
44+
mod = "exec-async" and cmdArg = 0
45+
or
46+
mod = "execa" and cmdArg = 0
47+
)
3948
or
40-
mod = "remote-exec" and cmdArg = 1
49+
shell = true and
50+
(
51+
mod = "exec" and
52+
cmdArg = 0
53+
or
54+
mod = "remote-exec" and cmdArg = 1
55+
)
4156
) and
4257
callee = DataFlow::moduleImport(mod)
4358
|
@@ -46,4 +61,8 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
4661
}
4762

4863
override DataFlow::Node getACommandArgument() { result = getArgument(cmdArg) }
64+
65+
override predicate isShellInterpreted(DataFlow::Node arg) {
66+
arg = getACommandArgument() and shell = true
67+
}
4968
}

0 commit comments

Comments
 (0)