Skip to content

Commit e163d8d

Browse files
authored
Merge pull request github#2796 from asger-semmle/js/partial-invoke-receiver
Approved by esbena
2 parents abbc929 + 9249b92 commit e163d8d

File tree

9 files changed

+172
-117
lines changed

9 files changed

+172
-117
lines changed

javascript/ql/src/semmle/javascript/Closure.qll

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ module Closure {
267267
result = this
268268
}
269269

270-
override DataFlow::Node getBoundReceiver() { result = getArgument(1) }
270+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
271+
callback = getArgument(0) and
272+
result = getArgument(1)
273+
}
271274
}
272275
}

javascript/ql/src/semmle/javascript/StandardLibrary.qll

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,33 +46,26 @@ class DirectEval extends CallExpr {
4646
}
4747

4848
/**
49-
* Flow analysis for `this` expressions inside a function that is called with
50-
* `Array.prototype.map` or a similar Array function that binds `this`.
51-
*
52-
* However, since the function could be invoked in another way, we additionally
53-
* still infer the ordinary abstract value.
49+
* Models `Array.prototype.map` and friends as partial invocations that pass their second
50+
* argument as the receiver to the callback.
5451
*/
55-
private class AnalyzedThisInArrayIterationFunction extends AnalyzedNode, DataFlow::ThisNode {
56-
AnalyzedNode thisSource;
57-
58-
AnalyzedThisInArrayIterationFunction() {
59-
exists(DataFlow::MethodCallNode bindingCall, string name |
52+
private class ArrayIterationCallbackAsPartialInvoke extends DataFlow::PartialInvokeNode::Range, DataFlow::MethodCallNode {
53+
ArrayIterationCallbackAsPartialInvoke() {
54+
getNumArgument() = 2 and
55+
// Filter out library methods named 'forEach' etc
56+
not DataFlow::moduleImport(_).flowsTo(getReceiver()) and
57+
exists(string name | name = getMethodName() |
6058
name = "filter" or
6159
name = "forEach" or
6260
name = "map" or
6361
name = "some" or
6462
name = "every"
65-
|
66-
name = bindingCall.getMethodName() and
67-
2 = bindingCall.getNumArgument() and
68-
getBinder() = bindingCall.getCallback(0) and
69-
thisSource = bindingCall.getArgument(1)
7063
)
7164
}
7265

73-
override AbstractValue getALocalValue() {
74-
result = thisSource.getALocalValue() or
75-
result = AnalyzedNode.super.getALocalValue()
66+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
67+
callback = getArgument(0) and
68+
result = getArgument(1)
7669
}
7770
}
7871

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,9 @@ class MidPathNode extends PathNode, MkMidNode {
13111311
or
13121312
// Skip the exceptional return on functions, as this highlights the entire function.
13131313
nd = any(DataFlow::FunctionNode f).getExceptionalReturn()
1314+
or
1315+
// Skip the synthetic 'this' node, as a ThisExpr will be the next node anyway
1316+
nd = DataFlow::thisNode(_)
13141317
}
13151318
}
13161319

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,13 @@ class PartialInvokeNode extends DataFlow::Node {
11991199

12001200
PartialInvokeNode() { this = range }
12011201

1202+
/** Gets a node holding a callback invoked by this partial invocation node. */
1203+
DataFlow::Node getACallbackNode() {
1204+
isPartialArgument(result, _, _)
1205+
or
1206+
exists(getBoundReceiver(result))
1207+
}
1208+
12021209
/**
12031210
* Holds if `argument` is passed as argument `index` to the function in `callback`.
12041211
*/
@@ -1216,7 +1223,12 @@ class PartialInvokeNode extends DataFlow::Node {
12161223
/**
12171224
* Gets the node holding the receiver to be passed to the bound function, if specified.
12181225
*/
1219-
DataFlow::Node getBoundReceiver() { result = range.getBoundReceiver() }
1226+
DataFlow::Node getBoundReceiver() { result = range.getBoundReceiver(_) }
1227+
1228+
/**
1229+
* Gets the node holding the receiver to be passed to the bound function, if specified.
1230+
*/
1231+
DataFlow::Node getBoundReceiver(DataFlow::Node callback) { result = range.getBoundReceiver(callback) }
12201232
}
12211233

12221234
module PartialInvokeNode {
@@ -1235,9 +1247,17 @@ module PartialInvokeNode {
12351247
DataFlow::SourceNode getBoundFunction(DataFlow::Node callback, int boundArgs) { none() }
12361248

12371249
/**
1250+
* DEPRECATED. Use the one-argument version of `getBoundReceiver` instead.
1251+
*
12381252
* Gets the node holding the receiver to be passed to the bound function, if specified.
12391253
*/
1254+
deprecated
12401255
DataFlow::Node getBoundReceiver() { none() }
1256+
1257+
/**
1258+
* Gets the node holding the receiver to be passed to `callback`.
1259+
*/
1260+
DataFlow::Node getBoundReceiver(DataFlow::Node callback) { none() }
12411261
}
12421262

12431263
/**
@@ -1264,7 +1284,8 @@ module PartialInvokeNode {
12641284
result = this
12651285
}
12661286

1267-
override DataFlow::Node getBoundReceiver() {
1287+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
1288+
callback = getReceiver() and
12681289
result = getArgument(0)
12691290
}
12701291
}
@@ -1309,6 +1330,22 @@ module PartialInvokeNode {
13091330
result = this
13101331
}
13111332
}
1333+
1334+
/**
1335+
* A call to `for-in` or `for-own`, passing the context parameter to the target function.
1336+
*/
1337+
class ForOwnInPartialCall extends PartialInvokeNode::Range, DataFlow::CallNode {
1338+
ForOwnInPartialCall() {
1339+
exists(string name | name = "for-in" or name = "for-own" |
1340+
this = moduleImport(name).getACall()
1341+
)
1342+
}
1343+
1344+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
1345+
callback = getArgument(1) and
1346+
result = getArgument(2)
1347+
}
1348+
}
13121349
}
13131350

13141351
/**

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private module CachedSteps {
9999
private predicate partiallyCalls(
100100
DataFlow::PartialInvokeNode invk, DataFlow::AnalyzedNode callback, Function f
101101
) {
102-
invk.isPartialArgument(callback, _, _) and
102+
callback = invk.getACallbackNode() and
103103
exists(AbstractFunction callee | callee = callback.getAValue() |
104104
if callback.getAValue().isIndefinite("global")
105105
then f = callee.getFunction() and f.getFile() = invk.getFile()
@@ -135,6 +135,12 @@ private module CachedSteps {
135135
not p.isRestParameter() and
136136
parm = DataFlow::parameterNode(p)
137137
)
138+
or
139+
exists(DataFlow::Node callback |
140+
arg = invk.(DataFlow::PartialInvokeNode).getBoundReceiver(callback) and
141+
partiallyCalls(invk, callback, f) and
142+
parm = DataFlow::thisNode(f)
143+
)
138144
}
139145

140146
/**

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,24 @@ private class TypeInferredMethodWithAnalyzedReturnFlow extends CallWithNonLocalA
274274

275275
override AnalyzedFunction getACallee() { result = fun }
276276
}
277+
278+
/**
279+
* Propagates receivers into locally defined callbacks of partial invocations.
280+
*/
281+
private class AnalyzedThisInPartialInvokeCallback extends AnalyzedNode, DataFlow::ThisNode {
282+
DataFlow::PartialInvokeNode call;
283+
DataFlow::Node receiver;
284+
285+
AnalyzedThisInPartialInvokeCallback() {
286+
exists(DataFlow::Node callbackArg |
287+
receiver = call.getBoundReceiver(callbackArg) and
288+
getBinder().flowsTo(callbackArg)
289+
)
290+
}
291+
292+
override AbstractValue getALocalValue() {
293+
result = receiver.analyze().getALocalValue()
294+
or
295+
result = AnalyzedNode.super.getALocalValue()
296+
}
297+
}

javascript/ql/src/semmle/javascript/frameworks/AngularJS/AngularJSCore.qll

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,5 +1097,8 @@ private class BindCall extends DataFlow::PartialInvokeNode::Range, DataFlow::Cal
10971097
result = this
10981098
}
10991099

1100-
override DataFlow::Node getBoundReceiver() { result = getArgument(0) }
1100+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
1101+
callback = getArgument(1) and
1102+
result = getArgument(0)
1103+
}
11011104
}

javascript/ql/src/semmle/javascript/frameworks/LodashUnderscore.qll

Lines changed: 71 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -407,112 +407,109 @@ module LodashUnderscore {
407407
* However, since the function could be invoked in another way, we additionally
408408
* still infer the ordinary abstract value.
409409
*/
410-
private class AnalyzedThisInBoundCallback extends AnalyzedNode, DataFlow::ThisNode {
411-
AnalyzedNode thisSource;
410+
private class LodashCallbackAsPartialInvoke extends DataFlow::PartialInvokeNode::Range,
411+
DataFlow::CallNode {
412+
int callbackIndex;
413+
int contextIndex;
412414

413-
AnalyzedThisInBoundCallback() {
414-
exists(
415-
DataFlow::CallNode bindingCall, string binderName, int callbackIndex, int contextIndex,
416-
int argumentCount
417-
|
418-
bindingCall = LodashUnderscore::member(binderName).getACall() and
419-
bindingCall.getNumArgument() = argumentCount and
420-
getBinder() = bindingCall.getCallback(callbackIndex) and
421-
thisSource = bindingCall.getArgument(contextIndex)
415+
LodashCallbackAsPartialInvoke() {
416+
exists(string name, int argumentCount |
417+
this = LodashUnderscore::member(name).getACall() and
418+
getNumArgument() = argumentCount
422419
|
423420
(
424-
binderName = "bind" or
425-
binderName = "callback" or
426-
binderName = "iteratee"
421+
name = "bind" or
422+
name = "callback" or
423+
name = "iteratee"
427424
) and
428425
callbackIndex = 0 and
429426
contextIndex = 1 and
430427
argumentCount = 2
431428
or
432429
(
433-
binderName = "all" or
434-
binderName = "any" or
435-
binderName = "collect" or
436-
binderName = "countBy" or
437-
binderName = "detect" or
438-
binderName = "dropRightWhile" or
439-
binderName = "dropWhile" or
440-
binderName = "each" or
441-
binderName = "eachRight" or
442-
binderName = "every" or
443-
binderName = "filter" or
444-
binderName = "find" or
445-
binderName = "findIndex" or
446-
binderName = "findKey" or
447-
binderName = "findLast" or
448-
binderName = "findLastIndex" or
449-
binderName = "findLastKey" or
450-
binderName = "forEach" or
451-
binderName = "forEachRight" or
452-
binderName = "forIn" or
453-
binderName = "forInRight" or
454-
binderName = "groupBy" or
455-
binderName = "indexBy" or
456-
binderName = "map" or
457-
binderName = "mapKeys" or
458-
binderName = "mapValues" or
459-
binderName = "max" or
460-
binderName = "min" or
461-
binderName = "omit" or
462-
binderName = "partition" or
463-
binderName = "pick" or
464-
binderName = "reject" or
465-
binderName = "remove" or
466-
binderName = "select" or
467-
binderName = "some" or
468-
binderName = "sortBy" or
469-
binderName = "sum" or
470-
binderName = "takeRightWhile" or
471-
binderName = "takeWhile" or
472-
binderName = "tap" or
473-
binderName = "thru" or
474-
binderName = "times" or
475-
binderName = "unzipWith" or
476-
binderName = "zipWith"
430+
name = "all" or
431+
name = "any" or
432+
name = "collect" or
433+
name = "countBy" or
434+
name = "detect" or
435+
name = "dropRightWhile" or
436+
name = "dropWhile" or
437+
name = "each" or
438+
name = "eachRight" or
439+
name = "every" or
440+
name = "filter" or
441+
name = "find" or
442+
name = "findIndex" or
443+
name = "findKey" or
444+
name = "findLast" or
445+
name = "findLastIndex" or
446+
name = "findLastKey" or
447+
name = "forEach" or
448+
name = "forEachRight" or
449+
name = "forIn" or
450+
name = "forInRight" or
451+
name = "groupBy" or
452+
name = "indexBy" or
453+
name = "map" or
454+
name = "mapKeys" or
455+
name = "mapValues" or
456+
name = "max" or
457+
name = "min" or
458+
name = "omit" or
459+
name = "partition" or
460+
name = "pick" or
461+
name = "reject" or
462+
name = "remove" or
463+
name = "select" or
464+
name = "some" or
465+
name = "sortBy" or
466+
name = "sum" or
467+
name = "takeRightWhile" or
468+
name = "takeWhile" or
469+
name = "tap" or
470+
name = "thru" or
471+
name = "times" or
472+
name = "unzipWith" or
473+
name = "zipWith"
477474
) and
478475
callbackIndex = 1 and
479476
contextIndex = 2 and
480477
argumentCount = 3
481478
or
482479
(
483-
binderName = "foldl" or
484-
binderName = "foldr" or
485-
binderName = "inject" or
486-
binderName = "reduce" or
487-
binderName = "reduceRight" or
488-
binderName = "transform"
480+
name = "foldl" or
481+
name = "foldr" or
482+
name = "inject" or
483+
name = "reduce" or
484+
name = "reduceRight" or
485+
name = "transform"
489486
) and
490487
callbackIndex = 1 and
491488
contextIndex = 3 and
492489
argumentCount = 4
493490
or
494491
(
495-
binderName = "sortedlastIndex"
492+
name = "sortedlastIndex"
496493
or
497-
binderName = "assign"
494+
name = "assign"
498495
or
499-
binderName = "eq"
496+
name = "eq"
500497
or
501-
binderName = "extend"
498+
name = "extend"
502499
or
503-
binderName = "merge"
500+
name = "merge"
504501
or
505-
binderName = "sortedIndex" and
506-
binderName = "uniq"
502+
name = "sortedIndex" and
503+
name = "uniq"
507504
) and
508505
callbackIndex = 2 and
509506
contextIndex = 3 and
510507
argumentCount = 4
511508
)
512509
}
513510

514-
override AbstractValue getALocalValue() {
515-
result = thisSource.getALocalValue() or
516-
result = AnalyzedNode.super.getALocalValue()
511+
override DataFlow::Node getBoundReceiver(DataFlow::Node callback) {
512+
callback = getArgument(callbackIndex) and
513+
result = getArgument(contextIndex)
517514
}
518515
}

0 commit comments

Comments
 (0)