Skip to content

Commit ec5896a

Browse files
committed
add additional data-flow edges to data-flow related to promises
1 parent ad92d6f commit ec5896a

9 files changed

Lines changed: 533 additions & 200 deletions

File tree

javascript/ql/src/semmle/javascript/Promises.qll

Lines changed: 372 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,380 @@
11
/**
2-
* Provides classes for modelling promise libraries.
2+
* Provides classes for modelling promises and their data-flow.
33
*/
44

55
import javascript
66

7+
/**
8+
* A definition of a `Promise` object.
9+
*/
10+
abstract class PromiseDefinition extends DataFlow::SourceNode {
11+
/** Gets the executor function of this promise object. */
12+
abstract DataFlow::FunctionNode getExecutor();
13+
14+
/** Gets the `resolve` parameter of the executor function. */
15+
DataFlow::ParameterNode getResolveParameter() { result = getExecutor().getParameter(0) }
16+
17+
/** Gets the `reject` parameter of the executor function. */
18+
DataFlow::ParameterNode getRejectParameter() { result = getExecutor().getParameter(1) }
19+
20+
/** Gets the `i`th callback handler installed by method `m`. */
21+
private DataFlow::FunctionNode getAHandler(string m, int i) {
22+
result = getAMethodCall(m).getCallback(i)
23+
}
24+
25+
/**
26+
* Gets a function that handles promise resolution, including both
27+
* `then` handlers and `finally` handlers.
28+
*/
29+
DataFlow::FunctionNode getAResolveHandler() {
30+
result = getAHandler("then", 0) or
31+
result = getAFinallyHandler()
32+
}
33+
34+
/**
35+
* Gets a function that handles promise rejection, including
36+
* `then` handlers, `catch` handlers and `finally` handlers.
37+
*/
38+
DataFlow::FunctionNode getARejectHandler() {
39+
result = getAHandler("then", 1) or
40+
result = getACatchHandler() or
41+
result = getAFinallyHandler()
42+
}
43+
44+
/**
45+
* Gets a `catch` handler of this promise.
46+
*/
47+
DataFlow::FunctionNode getACatchHandler() { result = getAHandler("catch", 0) }
48+
49+
/**
50+
* Gets a `finally` handler of this promise.
51+
*/
52+
DataFlow::FunctionNode getAFinallyHandler() { result = getAHandler("finally", 0) }
53+
}
54+
55+
/** Holds if the `i`th callback handler is installed by method `m`. */
56+
private predicate hasHandler(DataFlow::InvokeNode promise, string m, int i) {
57+
exists(promise.getAMethodCall(m).getCallback(i))
58+
}
59+
60+
/**
61+
* A call that looks like a Promise.
62+
*
63+
* For example, this could be the call `promise(f).then(function(v){...})`
64+
*/
65+
class PromiseCandidate extends DataFlow::InvokeNode {
66+
PromiseCandidate() {
67+
hasHandler(this, "then", [0 .. 1]) or
68+
hasHandler(this, "catch", 0) or
69+
hasHandler(this, "finally", 0)
70+
}
71+
}
72+
73+
/**
74+
* A promise object created by the standard ECMAScript 2015 `Promise` constructor.
75+
*/
76+
private class ES2015PromiseDefinition extends PromiseDefinition, DataFlow::NewNode {
77+
ES2015PromiseDefinition() { this = DataFlow::globalVarRef("Promise").getAnInstantiation() }
78+
79+
override DataFlow::FunctionNode getExecutor() { result = getCallback(0) }
80+
}
81+
82+
/**
83+
* A promise that is created and resolved with one or more value.
84+
*/
85+
abstract class PromiseCreationCall extends DataFlow::CallNode {
86+
/**
87+
* Gets the value this promise is resolved with.
88+
*/
89+
abstract DataFlow::Node getValue();
90+
}
91+
92+
/**
93+
* A promise that is created using a `.resolve()` call.
94+
*/
95+
abstract class ResolvedPromiseDefinition extends PromiseCreationCall { }
96+
97+
/**
98+
* A resolved promise created by the standard ECMAScript 2015 `Promise.resolve` function.
99+
*/
100+
class ResolvedES2015PromiseDefinition extends ResolvedPromiseDefinition {
101+
ResolvedES2015PromiseDefinition() {
102+
this = DataFlow::globalVarRef("Promise").getAMemberCall("resolve")
103+
}
104+
105+
override DataFlow::Node getValue() { result = getArgument(0) }
106+
}
107+
108+
/**
109+
* An aggregated promise produced either by `Promise.all` or `Promise.race`.
110+
*/
111+
class AggregateES2015PromiseDefinition extends PromiseCreationCall {
112+
AggregateES2015PromiseDefinition() {
113+
exists(string m | m = "all" or m = "race" |
114+
this = DataFlow::globalVarRef("Promise").getAMemberCall(m)
115+
)
116+
}
117+
118+
override DataFlow::Node getValue() {
119+
result = getArgument(0).getALocalSource().(DataFlow::ArrayCreationNode).getAnElement()
120+
}
121+
}
122+
123+
/**
124+
* This module defines how data-flow propagates into and out a Promise.
125+
*/
126+
private module PromiseFlow {
127+
/**
128+
* A promise from which data-flow can flow into or out of.
129+
*
130+
* This promise can both be a promise created by e.g. `new Promise(..)` or `Promise.resolve(..)`,
131+
* or the result from calling a method on a promise e.g. `promise.then(..)`.
132+
*
133+
* The 4 methods in this class describe that ordinary and exceptional flow can flow into and out of this promise.
134+
*/
135+
private abstract class PromiseNode extends DataFlow::SourceNode {
136+
137+
/**
138+
* Get a DataFlow::Node for a value that this promise is resolved with.
139+
* The value is sent either to a chained promise, or to an `await` expression.
140+
*
141+
* The value is e.g. an argument to `resolve(..)`, or a return value from a `.then(..)` handler.
142+
*/
143+
DataFlow::Node getASentResolveValue() { none() }
144+
145+
/**
146+
* Get the DataFlow::Node that receives the value that this promise has been resolved with.
147+
*
148+
* E.g. the `x` in `promise.then((x) => ..)`.
149+
*/
150+
DataFlow::Node getReceivedResolveValue() { none() }
151+
152+
/**
153+
* Get a DataFlow::Node for a value that this promise is rejected with.
154+
* The value is sent either to a chained promise, or thrown by an `await` expression.
155+
*
156+
* The value is e.g. an argument to `reject(..)`, or an exception thrown by the promise executor.
157+
*/
158+
DataFlow::Node getASentRejectValue() { none() }
159+
160+
/**
161+
* Get the DataFlow::Node that receives the value that this promise has been rejected with.
162+
*
163+
* E.g. the `x` in `promise.catch((x) => ..)`.
164+
*/
165+
DataFlow::Node getReceivedRejectValue() { none() }
166+
}
167+
168+
/**
169+
* A PromiseNode for a PromiseDefinition.
170+
* E.g. `new Promise(..)`.
171+
*/
172+
private class PromiseDefinitionNode extends PromiseNode {
173+
PromiseDefinition promise;
174+
175+
PromiseDefinitionNode() { this = promise }
176+
177+
override DataFlow::Node getASentResolveValue() {
178+
result = promise.getResolveParameter().getACall().getArgument(0)
179+
}
180+
181+
override DataFlow::Node getASentRejectValue() {
182+
result = promise.getRejectParameter().getACall().getArgument(0)
183+
or
184+
result = promise.getExecutor().getExceptionalReturn()
185+
}
186+
}
187+
188+
/**
189+
* A PromiseNode for a call that creates a promise.
190+
* E.g. `Promise.resolve(..)` or `Promise.all(..)`.
191+
*/
192+
private class PromiseCreationNode extends PromiseNode {
193+
PromiseCreationCall promise;
194+
195+
PromiseCreationNode() { this = promise }
196+
197+
override DataFlow::Node getASentResolveValue() {
198+
exists(DataFlow::Node value | value = promise.getValue() |
199+
not value instanceof PromiseNode and
200+
result = value
201+
or
202+
result = value.(PromiseNode).getASentResolveValue()
203+
)
204+
}
205+
206+
override DataFlow::Node getASentRejectValue() {
207+
result = promise.getValue().(PromiseNode).getASentRejectValue()
208+
}
209+
}
210+
211+
/**
212+
* A node referring to a PromiseNode through type-tracking.
213+
*/
214+
private class TrackedPromiseNode extends PromiseNode {
215+
PromiseNode base;
216+
TrackedPromiseNode() {
217+
this = trackPromise(DataFlow::TypeTracker::end(), base) and
218+
not this instanceof PromiseDefinitionNode and
219+
not this instanceof PromiseCreationNode
220+
}
221+
222+
override DataFlow::Node getASentResolveValue() { result = base.getASentResolveValue() }
223+
override DataFlow::Node getReceivedResolveValue() { result = base.getReceivedResolveValue() }
224+
override DataFlow::Node getASentRejectValue() { result = base.getASentRejectValue() }
225+
override DataFlow::Node getReceivedRejectValue() { result = base.getReceivedRejectValue() }
226+
}
227+
228+
private DataFlow::SourceNode trackPromise(DataFlow::TypeTracker t, PromiseNode promise) {
229+
t.start() and result = promise
230+
or
231+
exists(DataFlow::TypeTracker t2 | result = trackPromise(t2, promise).track(t2, t))
232+
}
233+
234+
/**
235+
* A PromiseNode that is a method call on an existing PromiseNode.
236+
* E.g. `promise.then(..)`.
237+
*/
238+
private abstract class ChainedPromiseNode extends PromiseNode, DataFlow::MethodCallNode {
239+
PromiseNode base;
240+
241+
ChainedPromiseNode() { this = base.getAMethodCall(_) }
242+
243+
PromiseNode getBase() { result = base }
244+
}
245+
246+
/**
247+
* A PromiseNode for the `.then(..)` method on an existing promise.
248+
*/
249+
private class PromiseThenNode extends ChainedPromiseNode {
250+
PromiseThenNode() { this = base.getAMethodCall("then") }
251+
252+
override DataFlow::Node getASentResolveValue() {
253+
exists(DataFlow::Node ret | ret = this.getCallback(0).getAReturn() |
254+
if ret instanceof PromiseNode
255+
then result = ret.(PromiseNode).getReceivedResolveValue()
256+
else result = ret
257+
)
258+
}
259+
260+
override DataFlow::Node getASentRejectValue() {
261+
not exists(this.getCallback(1)) and result = base.getASentRejectValue()
262+
or
263+
result = this.getCallback([0..1]).getExceptionalReturn()
264+
}
265+
266+
override DataFlow::Node getReceivedResolveValue() { result = this.getCallback(0).getParameter(0) }
267+
268+
override DataFlow::Node getReceivedRejectValue() { result = this.getCallback(1).getParameter(0) }
269+
}
270+
271+
/**
272+
* A PromiseNode for the `.finally(..)` method on an existing promise.
273+
*/
274+
private class PromiseFinallyNode extends ChainedPromiseNode {
275+
PromiseFinallyNode() { this = base.getAMethodCall("finally") }
276+
277+
override DataFlow::Node getASentResolveValue() { result = base.getASentResolveValue() }
278+
279+
override DataFlow::Node getASentRejectValue() {
280+
result = base.getASentRejectValue()
281+
or
282+
result = this.getCallback(0).getExceptionalReturn()
283+
}
284+
}
285+
286+
/**
287+
* A PromiseNode for the `.catch(..)` method on an existing promise.
288+
*/
289+
private class PromiseCatchNode extends ChainedPromiseNode {
290+
PromiseCatchNode() { this = base.getAMethodCall("catch") }
291+
292+
override DataFlow::Node getASentResolveValue() {
293+
exists(DataFlow::Node ret | ret = this.getCallback(0).getAReturn() |
294+
if ret instanceof PromiseNode
295+
then result = ret.(PromiseNode).getReceivedResolveValue()
296+
else result = ret
297+
)
298+
or
299+
result = base.getASentResolveValue()
300+
}
301+
302+
override DataFlow::Node getASentRejectValue() { result = this.getCallback(0).getExceptionalReturn() }
303+
304+
override DataFlow::Node getReceivedResolveValue() { none() }
305+
306+
override DataFlow::Node getReceivedRejectValue() { result = this.getCallback(0).getParameter(0) }
307+
}
308+
309+
310+
private ChainedPromiseNode getAChainedPromise(PromiseNode p) { result.getBase() = p}
311+
312+
/**
313+
* A data flow edge from a promise resolve/reject to the corresponding handler (or `await` expression).
314+
*/
315+
private class PromiseFlowStep extends DataFlow::AdditionalFlowStep {
316+
PromiseNode promise;
317+
318+
PromiseFlowStep() { this = promise }
319+
320+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
321+
pred = promise.getASentResolveValue() and
322+
succ = getAChainedPromise(promise).getReceivedResolveValue()
323+
or
324+
pred = promise.getASentRejectValue() and
325+
succ = getAChainedPromise(promise).getReceivedRejectValue()
326+
or
327+
pred = promise.getASentResolveValue() and
328+
exists(DataFlow::SourceNode awaitNode |
329+
awaitNode.asExpr().(AwaitExpr).getOperand() = promise.asExpr() and
330+
succ = awaitNode
331+
)
332+
or
333+
pred = promise.getASentRejectValue() and
334+
exists(DataFlow::SourceNode awaitNode |
335+
awaitNode.asExpr().(AwaitExpr).getOperand() = promise.asExpr() and
336+
succ = awaitNode.asExpr().getExceptionTarget()
337+
)
338+
}
339+
}
340+
}
341+
342+
/**
343+
* Holds if taint propagates from `pred` to `succ` through promises.
344+
*/
345+
predicate promiseTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
346+
// from `x` to `new Promise((res, rej) => res(x))`
347+
pred = succ.(PromiseDefinition).getResolveParameter().getACall().getArgument(0)
348+
or
349+
// from `x` to `Promise.resolve(x)`
350+
pred = succ.(PromiseCreationCall).getValue()
351+
or
352+
exists(DataFlow::MethodCallNode thn, DataFlow::FunctionNode cb |
353+
thn.getMethodName() = "then" and cb = thn.getCallback(0)
354+
|
355+
// from `p` to `x` in `p.then(x => ...)`
356+
pred = thn.getReceiver() and
357+
succ = cb.getParameter(0)
358+
or
359+
// from `v` to `p.then(x => return v)`
360+
pred = cb.getAReturn() and
361+
succ = thn
362+
)
363+
}
364+
365+
/**
366+
* An additional taint step that involves promises.
367+
*/
368+
private class PromiseTaintStep extends TaintTracking::AdditionalTaintStep {
369+
DataFlow::Node source;
370+
371+
PromiseTaintStep() { promiseTaintStep(source, this) }
372+
373+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
374+
pred = source and succ = this
375+
}
376+
}
377+
7378
/**
8379
* Provides classes for working with the `bluebird` library (http://bluebirdjs.com).
9380
*/

0 commit comments

Comments
 (0)