Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c20e24d

Browse files
authored
Merge pull request #385 from asger-semmle/async-model
Approved by xiemaisi
2 parents 282d1e2 + 799cd33 commit c20e24d

10 files changed

Lines changed: 368 additions & 1 deletion

File tree

change-notes/1.19/analysis-javascript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* Support for popular libraries has been improved. Consequently, queries may produce more results on code bases that use the following features:
1010
- file system access, for example through [fs-extra](https://github.com/jprichardson/node-fs-extra) or [globby](https://www.npmjs.com/package/globby)
1111
- outbound network access, for example through the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
12-
- the [Google Cloud Spanner](https://cloud.google.com/spanner), [lodash](https://lodash.com) and [underscore](https://underscorejs.org/) libraries
12+
- the [Google Cloud Spanner](https://cloud.google.com/spanner), [lodash](https://lodash.com), [underscore](https://underscorejs.org/), [async](https://www.npmjs.com/package/async) and [async-es](https://www.npmjs.com/package/async-es) libraries
1313

1414
* The type inference now handles nested imports (that is, imports not appearing at the toplevel). This may yield fewer false-positive results on projects that use this non-standard language feature.
1515

javascript/ql/src/javascript.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import semmle.javascript.dataflow.DataFlow
5252
import semmle.javascript.dataflow.TaintTracking
5353
import semmle.javascript.dataflow.TypeInference
5454
import semmle.javascript.frameworks.AngularJS
55+
import semmle.javascript.frameworks.AsyncPackage
5556
import semmle.javascript.frameworks.AWS
5657
import semmle.javascript.frameworks.Azure
5758
import semmle.javascript.frameworks.Babel

javascript/ql/src/semmle/javascript/dataflow/Nodes.qll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,16 @@ class FunctionNode extends DataFlow::ValueNode, DataFlow::DefaultSourceNode {
287287
result = getParameter(_)
288288
}
289289

290+
/** Gets the number of parameters declared on this function. */
291+
int getNumParameter() {
292+
result = count(astNode.getAParameter())
293+
}
294+
295+
/** Holds if the last parameter of this function is a rest parameter. */
296+
predicate hasRestParameter() {
297+
astNode.hasRestParameter()
298+
}
299+
290300
/** Gets the name of this function, if it has one. */
291301
string getName() { result = astNode.getName() }
292302

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/**
2+
* Provides classes for working with [async](https://www.npmjs.com/package/async).
3+
*/
4+
import javascript
5+
6+
module AsyncPackage {
7+
/**
8+
* Gets a reference the given member of the `async` or `async-es` package.
9+
*/
10+
pragma[noopt]
11+
DataFlow::SourceNode member(string name) {
12+
result = DataFlow::moduleMember("async", name) or
13+
result = DataFlow::moduleMember("async-es", name)
14+
}
15+
16+
/**
17+
* Gets a reference to the given member or one of its `Limit` or `Series` variants.
18+
*
19+
* For example, `memberVariant("map")` finds references to `map`, `mapLimit`, and `mapSeries`.
20+
*/
21+
DataFlow::SourceNode memberVariant(string name) {
22+
result = member(name) or
23+
result = member(name + "Limit") or
24+
result = member(name + "Series")
25+
}
26+
27+
/**
28+
* A call to `async.waterfall`.
29+
*/
30+
class Waterfall extends DataFlow::InvokeNode {
31+
Waterfall() {
32+
this = member("waterfall").getACall()
33+
}
34+
35+
/**
36+
* Gets the array of tasks, if it can be found.
37+
*/
38+
DataFlow::ArrayCreationNode getTaskArray() {
39+
result.flowsTo(getArgument(0))
40+
}
41+
42+
/**
43+
* Gets the callback to invoke after the last task in the array completes.
44+
*/
45+
DataFlow::FunctionNode getFinalCallback() {
46+
result.flowsTo(getArgument(1))
47+
}
48+
49+
/**
50+
* Gets the `n`th task, if it can be found.
51+
*/
52+
DataFlow::FunctionNode getTask(int n) {
53+
result.flowsTo(getTaskArray().getElement(n))
54+
}
55+
56+
/**
57+
* Gets the number of tasks.
58+
*/
59+
int getNumTasks() {
60+
result = strictcount(getTaskArray().getAnElement())
61+
}
62+
}
63+
64+
/**
65+
* Gets the last parameter declared by the given function, unless that's a rest parameter.
66+
*/
67+
private DataFlow::ParameterNode getLastParameter(DataFlow::FunctionNode function) {
68+
not function.hasRestParameter() and
69+
result = function.getParameter(function.getNumParameter() - 1)
70+
}
71+
72+
/**
73+
* An invocation of the callback in a waterfall task, passing arguments to the next waterfall task.
74+
*
75+
* Such a callback has the form `callback(err, result1, result2, ...)`. The error is propagated
76+
* to the first parameter of the final callback, while `result1, result2, ...` are propagated to
77+
* the parameters of the following task.
78+
*/
79+
private class WaterfallNextTaskCall extends DataFlow::AdditionalPartialInvokeNode {
80+
Waterfall waterfall;
81+
int n;
82+
83+
WaterfallNextTaskCall() {
84+
this = getLastParameter(waterfall.getTask(n)).getACall()
85+
}
86+
87+
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
88+
// Pass results to next task
89+
index >= 0 and
90+
argument = getArgument(index + 1) and
91+
callback = waterfall.getTask(n + 1)
92+
or
93+
// For the last task, pass results to the final callback
94+
index >= 1 and
95+
n = waterfall.getNumTasks() - 1 and
96+
argument = getArgument(index) and
97+
callback = waterfall.getFinalCallback()
98+
or
99+
// Always pass error to the final callback
100+
index = 0 and
101+
argument = getArgument(0) and
102+
callback = waterfall.getFinalCallback()
103+
}
104+
}
105+
106+
/**
107+
* A call that iterates over a collection with asynchronous operations.
108+
*/
109+
class IterationCall extends DataFlow::InvokeNode {
110+
string name;
111+
112+
IterationCall() {
113+
this = memberVariant(name).getACall() and
114+
(
115+
name = "concat" or
116+
name = "detect" or
117+
name = "each" or
118+
name = "eachOf" or
119+
name = "forEach" or
120+
name = "forEachOf" or
121+
name = "every" or
122+
name = "filter" or
123+
name = "groupBy" or
124+
name = "map" or
125+
name = "mapValues" or
126+
name = "reduce" or
127+
name = "reduceRight" or
128+
name = "reject" or
129+
name = "some" or
130+
name = "sortBy" or
131+
name = "transform"
132+
)
133+
}
134+
135+
/**
136+
* Gets the name of the iteration call, without the `Limit` or `Series` suffix.
137+
*/
138+
string getName() { result = name }
139+
140+
/**
141+
* Gets the node holding the collection being iterated over.
142+
*/
143+
DataFlow::Node getCollection() {
144+
result = getArgument(0)
145+
}
146+
147+
/**
148+
* Gets the node holding the function being called for each element in the collection.
149+
*/
150+
DataFlow::Node getIteratorCallback() {
151+
result = getArgument(getNumArgument() - 2)
152+
}
153+
154+
/**
155+
* Gets the node holding the function being invoked after iteration is complete.
156+
*/
157+
DataFlow::Node getFinalCallback() {
158+
result = getArgument(getNumArgument() - 1)
159+
}
160+
}
161+
162+
/**
163+
* A taint step from the collection into the iterator callback of an iteration call.
164+
*
165+
* For example: `data -> item` in `async.each(data, (item, cb) => {})`.
166+
*/
167+
private class IterationInputTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
168+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
169+
exists (DataFlow::FunctionNode iteratee |
170+
iteratee = getIteratorCallback() and // Require a closure to avoid spurious call/return mismatch.
171+
pred = getCollection() and
172+
succ = iteratee.getParameter(0))
173+
}
174+
}
175+
176+
/**
177+
* A taint step from the return value of an iterator callback to the result of the iteration
178+
* call.
179+
*
180+
* For example: `item + taint()` -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
181+
*/
182+
private class IterationOutputTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
183+
IterationOutputTaintStep() {
184+
name = "concat" or
185+
name = "map" or
186+
name = "reduce" or
187+
name = "reduceRight"
188+
}
189+
190+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
191+
exists (DataFlow::FunctionNode iteratee, DataFlow::FunctionNode final, int i |
192+
iteratee = getIteratorCallback().getALocalSource() and
193+
final = getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
194+
pred = getLastParameter(iteratee).getACall().getArgument(i) and
195+
succ = final.getParameter(i))
196+
}
197+
}
198+
199+
/**
200+
* A taint step from the input of an iteration call, directly to its output.
201+
*
202+
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
203+
*/
204+
private class IterationPreserveTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
205+
IterationPreserveTaintStep() {
206+
name = "sortBy"
207+
208+
// We don't currently include `filter` and `reject` as they could act as sanitizers.
209+
}
210+
211+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
212+
exists (DataFlow::FunctionNode final |
213+
final = getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
214+
pred = getCollection() and
215+
succ = final.getParameter(1))
216+
}
217+
}
218+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item |
2+
| map.js:10:13:10:20 | source() | map.js:12:14:12:17 | item |
3+
| map.js:20:19:20:26 | source() | map.js:23:27:23:32 | result |
4+
| map.js:26:13:26:20 | source() | map.js:28:27:28:32 | result |
5+
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result |
6+
| waterfall.js:7:30:7:37 | source() | waterfall.js:10:12:10:16 | taint |
7+
| waterfall.js:7:30:7:37 | source() | waterfall.js:19:10:19:14 | taint |
8+
| waterfall.js:27:18:27:25 | source() | waterfall.js:38:10:38:12 | err |
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import javascript
2+
3+
DataFlow::CallNode getACall(string name) {
4+
result.getCalleeName() = name
5+
}
6+
7+
class BasicConfig extends TaintTracking::Configuration {
8+
BasicConfig() { this = "BasicConfig" }
9+
10+
override
11+
predicate isSource(DataFlow::Node node) {
12+
node = getACall("source")
13+
}
14+
15+
override
16+
predicate isSink(DataFlow::Node node) {
17+
node = getACall("sink").getAnArgument()
18+
}
19+
}
20+
21+
from BasicConfig cfg, DataFlow::Node src, DataFlow::Node sink
22+
where cfg.hasFlow(src, sink)
23+
select src, sink
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
let async_ = require('async');
2+
3+
function source() {
4+
return 'TAINT'
5+
}
6+
function sink(x) {
7+
console.log(x)
8+
}
9+
10+
async_.each(
11+
[1, source(), 2],
12+
function (item, callback) {
13+
sink(item); // NOT OK
14+
callback(null, 'Hello ' + item);
15+
},
16+
function (err, result) {
17+
sink(err); // OK
18+
sink(result); // OK - 'each' does not propagate return value
19+
}
20+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
let async_ = require('async');
2+
3+
function source() {
4+
return 'TAINT'
5+
}
6+
function sink(x) {
7+
console.log(x)
8+
}
9+
10+
async_.map([source()],
11+
(item, cb) => {
12+
sink(item), // NOT OK
13+
cb(null, 'safe');
14+
},
15+
(err, result) => sink(result) // OK
16+
);
17+
18+
async_.map(['safe'],
19+
(item, cb) => {
20+
let src = source();
21+
cb(null, src);
22+
},
23+
(err, result) => sink(result) // NOT OK
24+
);
25+
26+
async_.map([source()],
27+
(item, cb) => cb(null, item.substring(1)),
28+
(err, result) => sink(result) // NOT OK
29+
);
30+
31+
async_.map(['safe'],
32+
(item, cb) => cb(null, item),
33+
(err, result) => sink(result) // OK
34+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
let async_ = require('async');
2+
3+
function source() {
4+
return 'TAINT'
5+
}
6+
function sink(x) {
7+
console.log(x)
8+
}
9+
10+
async_.sortBy(['zz', source()],
11+
(x, cb) => cb(x.length),
12+
(err, result) => sink(result)); // NOT OK
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
let async_ = require('async');
2+
3+
var source, sink, somethingWrong;
4+
5+
async_.waterfall([
6+
function(callback) {
7+
callback(null, 'safe', source());
8+
},
9+
function(safe, taint, callback) {
10+
sink(taint); // NOT OK
11+
sink(safe); // OK
12+
callback(null, taint, safe);
13+
},
14+
function(taint, safe, callback) {
15+
callback(null, taint, safe);
16+
}
17+
],
18+
function finalCallback(err, taint, safe) {
19+
sink(taint); // NOT OK
20+
sink(safe); // OK
21+
}
22+
);
23+
24+
async_.waterfall([
25+
function(callback) {
26+
if (somethingWrong()) {
27+
callback(source());
28+
} else {
29+
callback(null, 'safe');
30+
}
31+
},
32+
function(safe, callback) {
33+
sink(safe); // OK
34+
callback(null, safe);
35+
}
36+
],
37+
function(err, safe) {
38+
sink(err); // NOT OK
39+
sink(safe); // OK
40+
}
41+
);

0 commit comments

Comments
 (0)