Skip to content

Commit 7d961e1

Browse files
committed
do review improvements
1 parent 2c74dc2 commit 7d961e1

File tree

6 files changed

+616
-81
lines changed

6 files changed

+616
-81
lines changed

javascript/ql/lib/semmle/javascript/frameworks/Execa.qll

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,69 @@
44

55
import javascript
66
import semmle.javascript.security.dataflow.RequestForgeryCustomizations
7-
import semmle.javascript.security.dataflow.UrlConcatenation
87

98
/**
109
* Provide model for [Execa](https://github.com/sindresorhus/execa) package
1110
*/
1211
module Execa {
1312
/**
14-
* The Execa input file option
13+
* The Execa input file read and output file write
1514
*/
16-
class ExecaRead extends FileSystemReadAccess, DataFlow::Node {
17-
API::Node execaNode;
15+
class ExecaFileSystemAccess extends FileSystemReadAccess, DataFlow::Node {
16+
API::Node execaArg;
17+
boolean isPipedToFile;
1818

19-
ExecaRead() {
19+
ExecaFileSystemAccess() {
2020
(
21-
execaNode = API::moduleImport("execa").getMember("$").getParameter(0)
21+
execaArg = API::moduleImport("execa").getMember("$").getParameter(0) and
22+
isPipedToFile = false
2223
or
23-
execaNode =
24+
execaArg =
2425
API::moduleImport("execa")
2526
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
26-
.getParameter([0, 1, 2])
27+
.getParameter([0, 1, 2]) and
28+
isPipedToFile = false
29+
or
30+
execaArg =
31+
API::moduleImport("execa")
32+
.getMember(["execa", "execaCommand", "execaCommandSync", "execaSync"])
33+
.getReturn()
34+
.getMember(["pipeStdout", "pipeAll", "pipeStderr"])
35+
.getParameter(0) and
36+
isPipedToFile = true
2737
) and
28-
this = execaNode.asSink()
38+
this = execaArg.asSink()
2939
}
3040

31-
// data is the output of a command so IDK how it can be implemented
3241
override DataFlow::Node getADataNode() { none() }
3342

3443
override DataFlow::Node getAPathArgument() {
35-
result = execaNode.getMember("inputFile").asSink()
44+
result = execaArg.getMember("inputFile").asSink() and isPipedToFile = false
45+
or
46+
result = execaArg.asSink() and isPipedToFile = true
3647
}
3748
}
3849

3950
/**
4051
* A call to `execa.execa` or `execa.execaSync`
4152
*/
4253
class ExecaCall extends API::CallNode {
43-
string name;
54+
boolean isSync;
4455

4556
ExecaCall() {
4657
this = API::moduleImport("execa").getMember("execa").getACall() and
47-
name = "execa"
58+
isSync = false
4859
or
4960
this = API::moduleImport("execa").getMember("execaSync").getACall() and
50-
name = "execaSync"
61+
isSync = true
5162
}
52-
53-
/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
54-
string getName() { result = name }
5563
}
5664

5765
/**
5866
* The system command execution nodes for `execa.execa` or `execa.execaSync` functions
5967
*/
6068
class ExecaExec extends SystemCommandExecution, ExecaCall {
61-
ExecaExec() { name = ["execa", "execaSync"] }
69+
ExecaExec() { isSync = [false, true] }
6270

6371
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
6472

@@ -73,7 +81,15 @@ module Execa {
7381
isExecaShellEnable(this.getParameter(1))
7482
}
7583

76-
override predicate isSync() { name = "execaSync" }
84+
override DataFlow::Node getArgumentList() {
85+
// execa(cmd, [arg]);
86+
exists(DataFlow::Node arg | arg = this.getArgument(1) |
87+
// if it is a object then it is a option argument not command argument
88+
result = arg and not arg.asExpr() instanceof ObjectExpr
89+
)
90+
}
91+
92+
override predicate isSync() { isSync = true }
7793

7894
override DataFlow::Node getOptionsArg() {
7995
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
@@ -84,116 +100,97 @@ module Execa {
84100
* A call to `execa.$` or `execa.$.sync` tag functions
85101
*/
86102
private class ExecaScriptExpr extends DataFlow::ExprNode {
87-
string name;
103+
boolean isSync;
88104

89105
ExecaScriptExpr() {
90106
this.asExpr() =
91107
[
92108
API::moduleImport("execa").getMember("$"),
93109
API::moduleImport("execa").getMember("$").getReturn()
94110
].getAValueReachableFromSource().asExpr() and
95-
name = "ASync"
111+
isSync = false
96112
or
97113
this.asExpr() =
98114
[
99115
API::moduleImport("execa").getMember("$").getMember("sync"),
100116
API::moduleImport("execa").getMember("$").getMember("sync").getReturn()
101117
].getAValueReachableFromSource().asExpr() and
102-
name = "Sync"
118+
isSync = true
103119
}
104-
105-
/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
106-
string getName() { result = name }
107120
}
108121

109122
/**
110123
* The system command execution nodes for `execa.$` or `execa.$.sync` tag functions
111124
*/
112125
class ExecaScriptEec extends SystemCommandExecution, ExecaScriptExpr {
113-
ExecaScriptEec() { name = ["Sync", "ASync"] }
126+
ExecaScriptEec() { isSync = [false, true] }
114127

115128
override DataFlow::Node getACommandArgument() {
116-
result.asExpr() = templateLiteralChildAsSink(this.asExpr()).getChildExpr(0)
129+
exists(TemplateLiteral tl | isFirstTaggedTemplateParameter(this.asExpr(), tl) |
130+
result.asExpr() = tl.getChildExpr(0) and
131+
not result.asExpr().mayHaveStringValue(" ") // exclude whitespace
132+
)
117133
}
118134

119135
override predicate isShellInterpreted(DataFlow::Node arg) {
120-
// $({shell: true})`${sink} ${sink} .. ${sink}`
136+
// $({shell: true})`${cmd} ${arg0} ... ${arg1}`
121137
// ISSUE: $`cmd args` I can't reach the tag function argument easily
122-
exists(TemplateLiteral tmpL | templateLiteralChildAsSink(this.asExpr()) = tmpL |
123-
arg.asExpr() = tmpL.getAChildExpr+() and
124-
isExecaShellEnableWithExpr(this.asExpr().(CallExpr).getArgument(0))
138+
exists(TemplateLiteral tmpL | isFirstTaggedTemplateParameter(this.asExpr(), tmpL) |
139+
arg.asExpr() = tmpL.getAChildExpr() and
140+
isExecaShellEnableWithExpr(this.asExpr().(CallExpr).getArgument(0)) and
141+
not arg.asExpr().mayHaveStringValue(" ") // exclude whitespace
125142
)
126143
}
127144

128145
override DataFlow::Node getArgumentList() {
129-
// $`${Can Not Be sink} ${sink} .. ${sink}`
130-
exists(TemplateLiteral tmpL | templateLiteralChildAsSink(this.asExpr()) = tmpL |
131-
result.asExpr() = tmpL.getAChildExpr+() and
132-
not result.asExpr() = tmpL.getChildExpr(0)
146+
// $`${cmd} ${arg0} ... ${argn}`
147+
exists(TemplateLiteral tmpL | isFirstTaggedTemplateParameter(this.asExpr(), tmpL) |
148+
result.asExpr() = tmpL.getAChildExpr() and
149+
not result.asExpr() = tmpL.getChildExpr(0) and
150+
not result.asExpr().mayHaveStringValue(" ") // exclude whitespace
133151
)
134152
}
135153

136-
override predicate isSync() { name = "Sync" }
154+
override predicate isSync() { isSync = true }
137155

138156
override DataFlow::Node getOptionsArg() {
139-
result = this.asExpr().getAChildExpr*().flow() and result.asExpr() instanceof ObjectExpr
157+
result = this.asExpr().getAChildExpr().flow() and result.asExpr() instanceof ObjectExpr
140158
}
141159
}
142160

143161
/**
144162
* A call to `execa.execaCommandSync` or `execa.execaCommand`
145163
*/
146164
private class ExecaCommandCall extends API::CallNode {
147-
string name;
165+
boolean isSync;
148166

149167
ExecaCommandCall() {
150168
this = API::moduleImport("execa").getMember("execaCommandSync").getACall() and
151-
name = "execaCommandSync"
169+
isSync = true
152170
or
153171
this = API::moduleImport("execa").getMember("execaCommand").getACall() and
154-
name = "execaCommand"
172+
isSync = false
155173
}
156-
157-
/** Gets the name of the exported function, such as `rm` in `shelljs.rm()`. */
158-
string getName() { result = name }
159-
}
160-
161-
/**
162-
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
163-
*/
164-
class ExecaCommandExec2 extends SystemCommandExecution, DataFlow::CallNode {
165-
ExecaCommandExec2() { this = API::moduleImport("execa").getMember("execaCommand").getACall() }
166-
167-
override DataFlow::Node getACommandArgument() { result = this.getArgument(0) }
168-
169-
override DataFlow::Node getArgumentList() { result = this.getArgument(0) }
170-
171-
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getArgument(0) }
172-
173-
override predicate isSync() { none() }
174-
175-
override DataFlow::Node getOptionsArg() { result = this }
176174
}
177175

178176
/**
179177
* The system command execution nodes for `execa.execaCommand` or `execa.execaCommandSync` functions
180178
*/
181179
class ExecaCommandExec extends SystemCommandExecution, ExecaCommandCall {
182-
ExecaCommandExec() { name = ["execaCommand", "execaCommandSync"] }
180+
ExecaCommandExec() { isSync = [false, true] }
183181

184182
override DataFlow::Node getACommandArgument() {
185183
result = this.(DataFlow::CallNode).getArgument(0)
186184
}
187185

188186
override DataFlow::Node getArgumentList() {
189-
// execaCommand("echo " + sink);
190-
// execaCommand(`echo ${sink}`);
191-
result.asExpr() = this.getParameter(0).asSink().asExpr().getAChildExpr+() and
187+
// execaCommand(`${cmd} ${arg}`);
188+
result.asExpr() = this.getParameter(0).asSink().asExpr().getAChildExpr() and
192189
not result.asExpr() = this.getArgument(0).asExpr().getChildExpr(0)
193190
}
194191

195192
override predicate isShellInterpreted(DataFlow::Node arg) {
196-
// execaCommandSync(sink1 + sink2, {shell: true})
193+
// execaCommandSync(`${cmd} ${arg}`, {shell: true})
197194
arg.asExpr() = this.getArgument(0).asExpr().getAChildExpr+() and
198195
isExecaShellEnable(this.getParameter(1))
199196
or
@@ -203,22 +200,25 @@ module Execa {
203200
not exists(this.getArgument(0).asExpr().getChildExpr(1))
204201
}
205202

206-
override predicate isSync() { name = "execaCommandSync" }
203+
override predicate isSync() { isSync = true }
207204

208205
override DataFlow::Node getOptionsArg() {
209206
result = this.getLastArgument() and result.asExpr() instanceof ObjectExpr
210207
}
211208
}
212209

213210
// Holds if left parameter is the left child of a template literal and returns the template literal
214-
private TemplateLiteral templateLiteralChildAsSink(Expr left) {
211+
private predicate isFirstTaggedTemplateParameter(Expr left, TemplateLiteral templateLiteral) {
215212
exists(TaggedTemplateExpr parent |
216-
parent.getTemplate() = result and
213+
templateLiteral = parent.getTemplate() and
217214
left = parent.getChildExpr(0)
218215
)
219216
}
220217

221-
// Holds whether Execa has shell enabled options or not, get Parameter responsible for options
218+
/**
219+
* Holds whether Execa has shell enabled options or not, get Parameter responsible for options
220+
*/
221+
pragma[inline]
222222
private predicate isExecaShellEnable(API::Node n) {
223223
n.getMember("shell").asSink().asExpr().(BooleanLiteral).getValue() = "true"
224224
}

javascript/ql/test/library-tests/frameworks/Execa/Execa.ql

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)