Skip to content

Commit 8655e5a

Browse files
authored
Merge pull request #768 from xiemaisi/js/call-summaries
Approved by asger-semmle
2 parents 06d7953 + 0877ec8 commit 8655e5a

File tree

12 files changed

+243
-20
lines changed

12 files changed

+243
-20
lines changed

change-notes/1.20/analysis-javascript.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- server-side code, for example [hapi](https://hapijs.com/)
99
* File classification has been improved to recognize additional generated files, for example files from [HTML Tidy](html-tidy.org).
1010

11-
* The taint tracking library now recognizes flow through persistent storage, this may give more results for the security queries.
11+
* The taint tracking library now recognizes flow through persistent storage and callbacks in certain cases. This may give more results for the security queries.
1212

1313
## New queries
1414

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,11 @@ private predicate exploratoryFlowStep(
471471
) {
472472
basicFlowStep(pred, succ, _, cfg) or
473473
basicStoreStep(pred, succ, _) or
474-
loadStep(pred, succ, _)
474+
loadStep(pred, succ, _) or
475+
// the following two disjuncts taken together over-approximate flow through
476+
// higher-order calls
477+
callback(pred, succ) or
478+
succ = pred.(DataFlow::FunctionNode).getAParameter()
475479
}
476480

477481
/**
@@ -536,10 +540,7 @@ private predicate callInputStep(
536540
) {
537541
(
538542
isRelevant(pred, cfg) and
539-
exists(Parameter parm |
540-
argumentPassing(invk, pred, f, parm) and
541-
succ = DataFlow::parameterNode(parm)
542-
)
543+
argumentPassing(invk, pred, f, succ)
543544
or
544545
isRelevant(pred, cfg) and
545546
exists(SsaDefinition prevDef, SsaDefinition def |
@@ -655,6 +656,57 @@ private predicate flowThroughProperty(
655656
)
656657
}
657658

659+
/**
660+
* Holds if `arg` and `cb` are passed as arguments to a function which in turn
661+
* invokes `cb`, passing `arg` as its `i`th argument.
662+
*
663+
* All of this is done under configuration `cfg`, and `arg` flows along a path
664+
* summarized by `summary`, while `cb` is only tracked locally.
665+
*/
666+
private predicate higherOrderCall(
667+
DataFlow::Node arg, DataFlow::Node cb, int i, DataFlow::Configuration cfg, PathSummary summary
668+
) {
669+
exists (Function f, DataFlow::InvokeNode outer, DataFlow::InvokeNode inner, int j,
670+
DataFlow::Node innerArg, DataFlow::ParameterNode cbParm, PathSummary oldSummary |
671+
reachableFromInput(f, outer, arg, innerArg, cfg, oldSummary) and
672+
argumentPassing(outer, cb, f, cbParm) and
673+
innerArg = inner.getArgument(j) |
674+
// direct higher-order call
675+
cbParm.flowsTo(inner.getCalleeNode()) and
676+
i = j and
677+
summary = oldSummary
678+
or
679+
// indirect higher-order call
680+
exists (DataFlow::Node cbArg, PathSummary newSummary |
681+
cbParm.flowsTo(cbArg) and
682+
higherOrderCall(innerArg, cbArg, i, cfg, newSummary) and
683+
summary = oldSummary.append(PathSummary::call()).append(newSummary)
684+
)
685+
)
686+
}
687+
688+
/**
689+
* Holds if `pred` is passed as an argument to a function `f` which also takes a
690+
* callback parameter `cb` and then invokes `cb`, passing `pred` into parameter `succ`
691+
* of `cb`.
692+
*
693+
* All of this is done under configuration `cfg`, and `arg` flows along a path
694+
* summarized by `summary`, while `cb` is only tracked locally.
695+
*/
696+
private predicate flowIntoHigherOrderCall(
697+
DataFlow::Node pred, DataFlow::Node succ, DataFlow::Configuration cfg, PathSummary summary
698+
) {
699+
exists(
700+
DataFlow::Node fArg, DataFlow::FunctionNode cb,
701+
int i, PathSummary oldSummary
702+
|
703+
higherOrderCall(pred, fArg, i, cfg, oldSummary) and
704+
cb = fArg.getALocalSource() and
705+
succ = cb.getParameter(i) and
706+
summary = oldSummary.append(PathSummary::call())
707+
)
708+
}
709+
658710
/**
659711
* Holds if there is a flow step from `pred` to `succ` described by `summary`
660712
* under configuration `cfg`.
@@ -671,6 +723,9 @@ private predicate flowStep(
671723
or
672724
// Flow through a property write/read pair
673725
flowThroughProperty(pred, succ, cfg, summary)
726+
or
727+
// Flow into higher-order call
728+
flowIntoHigherOrderCall(pred, succ, cfg, summary)
674729
) and
675730
not cfg.isBarrier(succ) and
676731
not cfg.isBarrier(pred, succ) and

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class ParameterNode extends DataFlow::SourceNode {
1717

1818
/** Gets the name of this parameter. */
1919
string getName() { result = p.getName() }
20+
21+
/** Holds if this parameter is a rest parameter. */
22+
predicate isRestParameter() { p.isRestParameter() }
2023
}
2124

2225
/** A data flow node corresponding to a function invocation (with or without `new`). */

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ module TaintTracking {
782782
* A function that returns the result of a sanitizer check.
783783
*/
784784
private class SanitizingFunction extends Function {
785-
Parameter sanitizedParameter;
785+
DataFlow::ParameterNode sanitizedParameter;
786786

787787
SanitizerGuardNode sanitizer;
788788

@@ -806,11 +806,11 @@ module TaintTracking {
806806
or
807807
returnExpr = getAReturnedExpr()
808808
) and
809-
DataFlow::parameterNode(sanitizedParameter).flowsToExpr(e) and
809+
sanitizedParameter.flowsToExpr(e) and
810810
sanitizer.sanitizes(sanitizerOutcome, e)
811811
) and
812812
getNumParameter() = 1 and
813-
sanitizedParameter = getParameter(0)
813+
sanitizedParameter.getParameter() = getParameter(0)
814814
}
815815

816816
/**

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

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ private module NodeTracking {
100100
basicStoreStep(mid, nd, _)
101101
or
102102
loadStep(mid, nd, _)
103+
or
104+
callback(mid, nd)
105+
or
106+
nd = mid.(DataFlow::FunctionNode).getAParameter()
103107
)
104108
}
105109

@@ -113,10 +117,7 @@ private module NodeTracking {
113117
) {
114118
isRelevant(pred) and
115119
(
116-
exists(Parameter parm |
117-
argumentPassing(invk, pred, f, parm) and
118-
succ = DataFlow::parameterNode(parm)
119-
)
120+
argumentPassing(invk, pred, f, succ)
120121
or
121122
exists(SsaDefinition prevDef, SsaDefinition def |
122123
pred = DataFlow::ssaDefinitionNode(prevDef) and
@@ -206,6 +207,53 @@ private module NodeTracking {
206207
)
207208
}
208209

210+
/**
211+
* Holds if `arg` and `cb` are passed as arguments to a function which in turn
212+
* invokes `cb`, passing `arg` as its `i`th argument. `arg` flows along a path summarized
213+
* by `summary`, while `cb` is only tracked locally.
214+
*/
215+
private predicate higherOrderCall(
216+
DataFlow::Node arg, DataFlow::Node cb, int i, PathSummary summary
217+
) {
218+
exists (Function f, DataFlow::InvokeNode outer, DataFlow::InvokeNode inner, int j,
219+
DataFlow::Node innerArg, DataFlow::ParameterNode cbParm, PathSummary oldSummary |
220+
reachableFromInput(f, outer, arg, innerArg, oldSummary) and
221+
argumentPassing(outer, cb, f, cbParm) and
222+
innerArg = inner.getArgument(j) |
223+
// direct higher-order call
224+
cbParm.flowsTo(inner.getCalleeNode()) and
225+
i = j and
226+
summary = oldSummary
227+
or
228+
// indirect higher-order call
229+
exists (DataFlow::Node cbArg, PathSummary newSummary |
230+
cbParm.flowsTo(cbArg) and
231+
higherOrderCall(innerArg, cbArg, i, newSummary) and
232+
summary = oldSummary.append(PathSummary::call()).append(newSummary)
233+
)
234+
)
235+
}
236+
237+
/**
238+
* Holds if `pred` is passed as an argument to a function `f` which also takes a
239+
* callback parameter `cb` and then invokes `cb`, passing `pred` into parameter `succ`
240+
* of `cb`. `arg` flows along a path summarized by `summary`, while `cb` is only tracked
241+
* locally.
242+
*/
243+
private predicate flowIntoHigherOrderCall(
244+
DataFlow::Node pred, DataFlow::Node succ, PathSummary summary
245+
) {
246+
exists(
247+
DataFlow::Node fArg, DataFlow::FunctionNode cb,
248+
int i, PathSummary oldSummary
249+
|
250+
higherOrderCall(pred, fArg, i, oldSummary) and
251+
cb = fArg.getALocalSource() and
252+
succ = cb.getParameter(i) and
253+
summary = oldSummary.append(PathSummary::call())
254+
)
255+
}
256+
209257
/**
210258
* Holds if there is a flow step from `pred` to `succ` described by `summary`.
211259
*/
@@ -219,6 +267,9 @@ private module NodeTracking {
219267
or
220268
// Flow through a property write/read pair
221269
flowThroughProperty(pred, succ, summary)
270+
or
271+
// Flow into higher-order call
272+
flowIntoHigherOrderCall(pred, succ, summary)
222273
}
223274

224275
/**

javascript/ql/src/semmle/javascript/dataflow/internal/FlowSteps.qll

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,19 @@ predicate localFlowStep(
8888
* through invocation `invk` of function `f`.
8989
*/
9090
predicate argumentPassing(
91-
DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Function f, Parameter parm
91+
DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Function f, DataFlow::ParameterNode parm
9292
) {
9393
calls(invk, f) and
9494
exists(int i |
95-
f.getParameter(i) = parm and
95+
f.getParameter(i) = parm.getParameter() and
9696
not parm.isRestParameter() and
9797
arg = invk.getArgument(i)
9898
)
9999
or
100100
exists(DataFlow::Node callback, int i |
101101
invk.(DataFlow::AdditionalPartialInvokeNode).isPartialArgument(callback, arg, i) and
102102
partiallyCalls(invk, callback, f) and
103-
parm = f.getParameter(i) and
103+
parm.getParameter() = f.getParameter(i) and
104104
not parm.isRestParameter()
105105
)
106106
}
@@ -110,10 +110,7 @@ predicate argumentPassing(
110110
* to a function call.
111111
*/
112112
predicate callStep(DataFlow::Node pred, DataFlow::Node succ) {
113-
exists(Parameter parm |
114-
argumentPassing(_, pred, _, parm) and
115-
succ = DataFlow::parameterNode(parm)
116-
)
113+
argumentPassing(_, pred, _, succ)
117114
}
118115

119116
/**
@@ -239,6 +236,34 @@ predicate loadStep(DataFlow::Node pred, DataFlow::PropRead succ, string prop) {
239236
succ.accesses(pred, prop)
240237
}
241238

239+
/**
240+
* Holds if there is a higher-order call with argument `arg`, and `cb` is the local
241+
* source of an argument that flows into the callee position of that call:
242+
*
243+
* ```
244+
* function f(x, g) {
245+
* g(
246+
* x // arg
247+
* );
248+
* }
249+
*
250+
* function cb() { // cb
251+
* }
252+
*
253+
* f(arg, cb);
254+
*
255+
* This is an over-approximation of a possible data flow step through a callback
256+
* invocation.
257+
*/
258+
predicate callback(DataFlow::Node arg, DataFlow::SourceNode cb) {
259+
exists (DataFlow::InvokeNode invk, DataFlow::ParameterNode cbParm, DataFlow::Node cbArg |
260+
arg = invk.getAnArgument() and
261+
cbParm.flowsTo(invk.getCalleeNode()) and
262+
callStep(cbArg, cbParm) and
263+
cb.flowsTo(cbArg)
264+
)
265+
}
266+
242267
/**
243268
* A utility class that is equivalent to `boolean` but does not require type joining.
244269
*/

javascript/ql/test/library-tests/InterProceduralFlow/DataFlow.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
22
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
33
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
4+
| callback.js:16:14:16:21 | "source" | callback.js:13:14:13:14 | x |
5+
| callback.js:27:15:27:23 | "source3" | callback.js:13:14:13:14 | x |
46
| destructuring.js:2:16:2:24 | "tainted" | destructuring.js:9:15:9:22 | tainted2 |
57
| destructuring.js:19:15:19:23 | "tainted" | destructuring.js:14:15:14:15 | p |
68
| destructuring.js:20:15:20:28 | "also tainted" | destructuring.js:15:15:15:15 | r |

javascript/ql/test/library-tests/InterProceduralFlow/GermanFlow.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
22
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
33
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
4+
| callback.js:16:14:16:21 | "source" | callback.js:13:14:13:14 | x |
5+
| callback.js:27:15:27:23 | "source3" | callback.js:13:14:13:14 | x |
46
| custom.js:1:14:1:26 | "verschmutzt" | custom.js:2:15:2:20 | quelle |
57
| destructuring.js:2:16:2:24 | "tainted" | destructuring.js:9:15:9:22 | tainted2 |
68
| destructuring.js:19:15:19:23 | "tainted" | destructuring.js:14:15:14:15 | p |

javascript/ql/test/library-tests/InterProceduralFlow/TaintTracking.expected

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
| a.js:1:15:1:23 | "tainted" | b.js:4:13:4:40 | whoKnow ... Tainted |
22
| a.js:1:15:1:23 | "tainted" | b.js:6:13:6:13 | x |
33
| a.js:2:15:2:28 | "also tainted" | b.js:5:13:5:29 | notTaintedTrustMe |
4+
| callback.js:16:14:16:21 | "source" | callback.js:13:14:13:14 | x |
5+
| callback.js:17:15:17:23 | "source2" | callback.js:13:14:13:14 | x |
6+
| callback.js:27:15:27:23 | "source3" | callback.js:13:14:13:14 | x |
47
| destructuring.js:2:16:2:24 | "tainted" | destructuring.js:5:14:5:20 | tainted |
58
| destructuring.js:2:16:2:24 | "tainted" | destructuring.js:9:15:9:22 | tainted2 |
69
| destructuring.js:19:15:19:23 | "tainted" | destructuring.js:14:15:14:15 | p |
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
function call(f, x) {
2+
return f(x);
3+
}
4+
5+
function map(f, xs) {
6+
const res = [];
7+
for (let i=0;i<xs.length;++i)
8+
res[i] = f(xs[i]);
9+
return res;
10+
}
11+
12+
function store(x) {
13+
let sink = x;
14+
}
15+
16+
let source = "source";
17+
let source2 = "source2";
18+
call(store, source);
19+
call(store, confounder); // call with different argument to make sure the call graph builder
20+
// doesn't resolve the call on line 2 for us
21+
map(store, [source2]);
22+
23+
function call2(x, f) {
24+
call(f, x);
25+
}
26+
27+
let source3 = "source3";
28+
call2(source3, store);
29+
call2(source3, confounder);
30+
31+
// semmle-extractor-options: --source-type module

0 commit comments

Comments
 (0)