Skip to content

Commit 473787a

Browse files
committed
refactor the getOptionsArg predicate into the SystemCommandExecution class
1 parent 75c1852 commit 473787a

7 files changed

Lines changed: 65 additions & 25 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ abstract class SystemCommandExecution extends DataFlow::Node {
2525

2626
/** Holds if the command execution happens synchronously. */
2727
abstract predicate isSync();
28+
29+
/**
30+
* Gets the data-flow node (if it exists) for an options argument.
31+
*/
32+
abstract DataFlow::Node getOptionsArg();
2833
}
2934

3035
/**

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,18 @@ module NodeJSLib {
627627
override predicate isSync() {
628628
"Sync" = methodName.suffix(methodName.length() - 4)
629629
}
630+
631+
override DataFlow::Node getOptionsArg() {
632+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
633+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode and // looks like argumentlist
634+
not result = getArgument(0) and
635+
// fork/spawn and all sync methos always has options as the last argument
636+
if methodName.regexpMatch("fork.*") or methodName.regexpMatch("spawn.*") or methodName.regexpMatch(".*Sync") then
637+
result = getLastArgument()
638+
else
639+
// the rest (exec/execFile) has the options argument as their second last.
640+
result = getArgument(this.getNumArgument() - 2)
641+
}
630642
}
631643

632644
/**

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@ module ShellJS {
162162
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getACommandArgument() }
163163

164164
override predicate isSync() {none ()}
165+
166+
override DataFlow::Node getOptionsArg() {
167+
result = getLastArgument() and
168+
not result = getArgument(0) and
169+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
170+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
171+
}
165172
}
166173

167174
/**

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@ import javascript
77

88
private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::InvokeNode {
99
int cmdArg;
10+
int optionsArg;
1011

1112
boolean shell;
1213
boolean sync;
1314

1415
SystemCommandExecutors() {
1516
exists(string mod, DataFlow::SourceNode callee |
1617
exists(string method |
17-
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false
18+
mod = "cross-spawn" and method = "sync" and cmdArg = 0 and shell = false and optionsArg = -1
1819
or
19-
mod = "execa" and
20+
mod = "execa" and optionsArg = -1 and
2021
(
2122
shell = false and
2223
(
@@ -40,21 +41,21 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
4041
(
4142
shell = false and
4243
(
43-
mod = "cross-spawn" and cmdArg = 0
44+
mod = "cross-spawn" and cmdArg = 0 and optionsArg = -1
4445
or
45-
mod = "cross-spawn-async" and cmdArg = 0
46+
mod = "cross-spawn-async" and cmdArg = 0 and optionsArg = -1
4647
or
47-
mod = "exec-async" and cmdArg = 0
48+
mod = "exec-async" and cmdArg = 0 and optionsArg = -1
4849
or
49-
mod = "execa" and cmdArg = 0
50+
mod = "execa" and cmdArg = 0 and optionsArg = -1
5051
)
5152
or
5253
shell = true and
5354
(
54-
mod = "exec" and
55+
mod = "exec" and optionsArg = -2 and
5556
cmdArg = 0
5657
or
57-
mod = "remote-exec" and cmdArg = 1
58+
mod = "remote-exec" and cmdArg = 1 and optionsArg = -1
5859
)
5960
) and
6061
callee = DataFlow::moduleImport(mod)
@@ -69,8 +70,16 @@ private class SystemCommandExecutors extends SystemCommandExecution, DataFlow::I
6970
arg = getACommandArgument() and shell = true
7071
}
7172

72-
override predicate isSync() {
73-
sync = true
73+
override predicate isSync() { sync = true }
74+
75+
override DataFlow::Node getOptionsArg() {
76+
(if optionsArg < 0 then
77+
result = getArgument(getNumArgument() - optionsArg)
78+
else
79+
result = getArgument(optionsArg)) and
80+
not result = getArgument(0) and
81+
not result.getALocalSource() instanceof DataFlow::FunctionNode and // looks like callback
82+
not result.getALocalSource() instanceof DataFlow::ArrayCreationNode // looks like argumentlist
7483
}
7584
}
7685

javascript/ql/src/semmle/javascript/security/UselessUseOfCat.qll

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,7 @@ private class CommandCall extends DataFlow::InvokeNode {
4141
* Gets the data-flow node (if it exists) for an options argument for an `exec`-like call.
4242
*/
4343
DataFlow::Node getOptionsArg() {
44-
exists(int n |
45-
n >= 1 and
46-
// if there is a command-list, then the options is at least the third argument.
47-
(not exists(command.getArgumentList()) or n >= 2) and
48-
// async exec calls can have a callback as their last call.
49-
if command.isSync() or not exists(getCallback())
50-
then n < getNumArgument()
51-
else n < getNumArgument() - 1
52-
|
53-
result = getArgument(n)
54-
)
55-
or
56-
// Fallback in case normal API conventions are broken.
57-
result = getAnArgument() and
58-
result.getALocalSource() instanceof DataFlow::ObjectLiteralNode
44+
result = command.getOptionsArg()
5945
}
6046

6147
/**

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ syncCommand
7171
| uselesscat.js:100:1:100:56 | execFil ... ptions) |
7272
| uselesscat.js:104:1:104:31 | execFil ... cat` ]) |
7373
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) |
74+
options
75+
| child_process-test.js:53:5:53:59 | cp.spaw ... cmd])) | child_process-test.js:53:25:53:58 | ['/C', ... , cmd]) |
76+
| child_process-test.js:54:5:54:50 | cp.spaw ... t(cmd)) | child_process-test.js:54:25:54:49 | ['/C', ... at(cmd) |
77+
| child_process-test.js:64:3:64:21 | cp.spawn(cmd, args) | child_process-test.js:64:17:64:20 | args |
78+
| uselesscat.js:28:1:28:39 | execSyn ... 1000}) | uselesscat.js:28:28:28:38 | {uid: 1000} |
79+
| uselesscat.js:30:1:30:64 | exec('c ... t) { }) | uselesscat.js:30:26:30:38 | { cwd: './' } |
80+
| uselesscat.js:34:1:34:54 | execSyn ... utf8'}) | uselesscat.js:34:36:34:53 | {encoding: 'utf8'} |
81+
| uselesscat.js:36:1:36:77 | execSyn ... utf8'}) | uselesscat.js:36:36:36:76 | { uid: ... 'utf8'} |
82+
| uselesscat.js:69:1:72:2 | execFil ... ut);\\n}) | uselesscat.js:69:38:69:55 | {encoding: 'utf8'} |
83+
| uselesscat.js:74:1:74:60 | execFil ... utf8'}) | uselesscat.js:74:42:74:59 | {encoding: 'utf8'} |
84+
| uselesscat.js:79:1:79:46 | execFil ... opts) | uselesscat.js:79:42:79:45 | opts |
85+
| uselesscat.js:82:1:82:90 | execFil ... String) | uselesscat.js:82:42:82:89 | anOptsF ... oString |
86+
| uselesscat.js:84:1:84:115 | execFil ... ring'}) | uselesscat.js:84:42:84:114 | {encodi ... tring'} |
87+
| uselesscat.js:86:1:86:75 | execFil ... utf8'}) | uselesscat.js:86:57:86:74 | {encoding: 'utf8'} |
88+
| uselesscat.js:100:1:100:56 | execFil ... ptions) | uselesscat.js:100:42:100:55 | unknownOptions |
89+
| uselesscat.js:111:1:111:51 | spawn(' ... it'] }) | uselesscat.js:111:14:111:50 | { stdio ... rit'] } |
90+
| uselesscat.js:136:17:138:2 | execSyn ... tf8'\\n}) | uselesscat.js:136:51:138:1 | { // NO ... utf8'\\n} |
7491
#select
7592
| False negative | uselesscat.js:54:42:54:69 | // NOT ... lagged] |
7693
| False positive | uselesscat.js:44:37:44:85 | // OK [ ... le read |

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ query string readFile(UselessCat cat) { result = PrettyPrintCatCall::createReadF
2121

2222
query SystemCommandExecution syncCommand() {
2323
result.isSync()
24+
}
25+
26+
query DataFlow::Node options(SystemCommandExecution sys) {
27+
result = sys.getOptionsArg()
2428
}

0 commit comments

Comments
 (0)