Skip to content

Commit 65102a0

Browse files
authored
Merge pull request #19770 from trailofbits/VF/async-package-improvements
Improve data flow in the `async` package
2 parents f587273 + 575da5c commit 65102a0

File tree

5 files changed

+127
-44
lines changed

5 files changed

+127
-44
lines changed

javascript/ql/lib/semmle/javascript/dataflow/internal/FlowSummaryPrivate.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ private string encodeContentAux(ContentSet cs, string arg) {
9494
cs = ContentSet::iteratorElement() and result = "IteratorElement"
9595
or
9696
cs = ContentSet::iteratorError() and result = "IteratorError"
97+
or
98+
cs = ContentSet::anyProperty() and result = "AnyMember"
9799
)
98100
or
99101
cs = getPromiseContent(arg) and

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

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ module AsyncPackage {
2525
result = member(name + "Series")
2626
}
2727

28+
/**
29+
* Gets `Limit` or `Series` name variants for a given member name.
30+
*
31+
* For example, `memberNameVariant("map")` returns `map`, `mapLimit`, and `mapSeries`.
32+
*/
33+
bindingset[name]
34+
private string memberNameVariant(string name) {
35+
result = name or
36+
result = name + "Limit" or
37+
result = name + "Series"
38+
}
39+
2840
/**
2941
* A call to `async.waterfall`.
3042
*/
@@ -101,22 +113,47 @@ module AsyncPackage {
101113
*/
102114
class IterationCall extends DataFlow::InvokeNode {
103115
string name;
116+
int iteratorCallbackIndex;
117+
int finalCallbackIndex;
104118

105119
IterationCall() {
106-
this = memberVariant(name).getACall() and
107-
name =
108-
[
109-
"concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
110-
"groupBy", "map", "mapValues", "reduce", "reduceRight", "reject", "some", "sortBy",
111-
"transform"
112-
]
120+
(
121+
(
122+
name =
123+
memberNameVariant([
124+
"concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
125+
"groupBy", "map", "mapValues", "reject", "some", "sortBy",
126+
]) and
127+
if name.matches("%Limit")
128+
then (
129+
iteratorCallbackIndex = 2 and finalCallbackIndex = 3
130+
) else (
131+
iteratorCallbackIndex = 1 and finalCallbackIndex = 2
132+
)
133+
)
134+
or
135+
name = ["reduce", "reduceRight", "transform"] and
136+
iteratorCallbackIndex = 2 and
137+
finalCallbackIndex = 3
138+
) and
139+
this = member(name).getACall()
113140
}
114141

115142
/**
116-
* Gets the name of the iteration call, without the `Limit` or `Series` suffix.
143+
* Gets the name of the iteration call
117144
*/
118145
string getName() { result = name }
119146

147+
/**
148+
* Gets the iterator callback index
149+
*/
150+
int getIteratorCallbackIndex() { result = iteratorCallbackIndex }
151+
152+
/**
153+
* Gets the final callback index
154+
*/
155+
int getFinalCallbackIndex() { result = finalCallbackIndex }
156+
120157
/**
121158
* Gets the node holding the collection being iterated over.
122159
*/
@@ -125,41 +162,48 @@ module AsyncPackage {
125162
/**
126163
* Gets the node holding the function being called for each element in the collection.
127164
*/
128-
DataFlow::Node getIteratorCallback() { result = this.getArgument(this.getNumArgument() - 2) }
165+
DataFlow::FunctionNode getIteratorCallback() {
166+
result = this.getCallback(iteratorCallbackIndex)
167+
}
129168

130169
/**
131-
* Gets the node holding the function being invoked after iteration is complete.
170+
* Gets the node holding the function being invoked after iteration is complete. (may not exist)
132171
*/
133-
DataFlow::Node getFinalCallback() { result = this.getArgument(this.getNumArgument() - 1) }
172+
DataFlow::FunctionNode getFinalCallback() { result = this.getCallback(finalCallbackIndex) }
134173
}
135174

136-
/**
137-
* A taint step from the collection into the iterator callback of an iteration call.
138-
*
139-
* For example: `data -> item` in `async.each(data, (item, cb) => {})`.
140-
*/
141-
private class IterationInputTaintStep extends TaintTracking::SharedTaintStep {
142-
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
143-
exists(DataFlow::FunctionNode iteratee, IterationCall call |
144-
iteratee = call.getIteratorCallback() and // Require a closure to avoid spurious call/return mismatch.
145-
pred = call.getCollection() and // TODO: needs a flow summary to ensure ArrayElement content is unfolded
146-
succ = iteratee.getParameter(0)
147-
)
175+
private class IterationCallFlowSummary extends DataFlow::SummarizedCallable {
176+
private int callbackArgIndex;
177+
178+
IterationCallFlowSummary() {
179+
this = "async.IteratorCall(callbackArgIndex=" + callbackArgIndex + ")" and
180+
callbackArgIndex in [1 .. 3]
181+
}
182+
183+
override DataFlow::InvokeNode getACallSimple() {
184+
result instanceof IterationCall and
185+
result.(IterationCall).getIteratorCallbackIndex() = callbackArgIndex
186+
}
187+
188+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
189+
preservesValue = true and
190+
input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
191+
output = "Argument[" + callbackArgIndex + "].Parameter[0]"
148192
}
149193
}
150194

151195
/**
152196
* A taint step from the return value of an iterator callback to the result of the iteration
153197
* call.
154198
*
155-
* For example: `item + taint()` -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
199+
* For example: `item + taint() -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
156200
*/
157201
private class IterationOutputTaintStep extends TaintTracking::SharedTaintStep {
158202
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
159203
exists(
160204
DataFlow::FunctionNode iteratee, DataFlow::FunctionNode final, int i, IterationCall call
161205
|
162-
iteratee = call.getIteratorCallback().getALocalSource() and
206+
iteratee = call.getIteratorCallback() and
163207
final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
164208
pred = getLastParameter(iteratee).getACall().getArgument(i) and
165209
succ = final.getParameter(i) and
@@ -175,14 +219,18 @@ module AsyncPackage {
175219
*
176220
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
177221
*/
178-
private class IterationPreserveTaintStep extends TaintTracking::SharedTaintStep {
179-
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
180-
exists(DataFlow::FunctionNode final, IterationCall call |
181-
final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
182-
pred = call.getCollection() and
183-
succ = final.getParameter(1) and
184-
call.getName() = "sortBy"
185-
)
222+
private class IterationPreserveTaintStepFlowSummary extends DataFlow::SummarizedCallable {
223+
IterationPreserveTaintStepFlowSummary() { this = "async.sortBy" }
224+
225+
override DataFlow::InvokeNode getACallSimple() {
226+
result instanceof IterationCall and
227+
result.(IterationCall).getName() = "sortBy"
228+
}
229+
230+
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
231+
preservesValue = false and
232+
input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
233+
output = "Argument[2].Parameter[1]"
186234
}
187235
}
188236
}
Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
legacyDataFlowDifference
2-
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with OLD data flow library |
3-
| map.js:10:13:10:20 | source() | map.js:12:14:12:17 | item | only flow with OLD data flow library |
4-
| map.js:26:13:26:20 | source() | map.js:28:27:28:32 | result | only flow with OLD data flow library |
5-
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with OLD data flow library |
2+
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with NEW data flow library |
3+
| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item | only flow with NEW data flow library |
4+
| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result | only flow with NEW data flow library |
5+
| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
6+
| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
7+
| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
8+
| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
9+
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with NEW data flow library |
610
#select
7-
| map.js:20:19:20:26 | source() | map.js:23:27:23:32 | result |
8-
| waterfall.js:8:30:8:37 | source() | waterfall.js:11:12:11:16 | taint |
9-
| waterfall.js:8:30:8:37 | source() | waterfall.js:20:10:20:14 | taint |
10-
| waterfall.js:28:18:28:25 | source() | waterfall.js:39:10:39:12 | err |
11-
| waterfall.js:46:22:46:29 | source() | waterfall.js:49:12:49:16 | taint |
12-
| waterfall.js:46:22:46:29 | source() | waterfall.js:55:10:55:14 | taint |
11+
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item |
12+
| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item |
13+
| map.js:24:19:24:26 | source() | map.js:27:27:27:32 | result |
14+
| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result |
15+
| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x |
16+
| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x |
17+
| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x |
18+
| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x |
19+
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result |
20+
| waterfall.js:16:30:16:37 | source() | waterfall.js:19:12:19:16 | taint |
21+
| waterfall.js:16:30:16:37 | source() | waterfall.js:28:10:28:14 | taint |
22+
| waterfall.js:36:18:36:25 | source() | waterfall.js:47:10:47:12 | err |
23+
| waterfall.js:54:22:54:29 | source() | waterfall.js:57:12:57:16 | taint |
24+
| waterfall.js:54:22:54:29 | source() | waterfall.js:63:10:63:14 | taint |

javascript/ql/test/library-tests/frameworks/AsyncPackage/map.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ function sink(x) {
77
console.log(x)
88
}
99

10+
function call_sink(x) {
11+
sink(x)
12+
}
13+
1014
async_.map([source()],
1115
(item, cb) => {
1216
sink(item), // NOT OK
@@ -32,3 +36,12 @@ async_.map(['safe'],
3236
(item, cb) => cb(null, item),
3337
(err, result) => sink(result) // OK
3438
);
39+
40+
async_.map([source()], call_sink) // NOT OK
41+
42+
async_.map(source().prop, call_sink) // NOT OK
43+
44+
async_.map({a: source()}, call_sink) // NOT OK
45+
46+
async_.mapLimit([source()], 1, call_sink) // NOT OK
47+

javascript/ql/test/library-tests/frameworks/AsyncPackage/waterfall.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
let async_ = require('async');
22
let waterfall = require('a-sync-waterfall');
33

4-
var source, sink, somethingWrong;
4+
function source() {
5+
return 'TAINT'
6+
}
7+
8+
function sink(x) {
9+
console.log(x)
10+
}
11+
12+
var somethingWrong;
513

614
async_.waterfall([
715
function(callback) {

0 commit comments

Comments
 (0)