Skip to content

Commit a3cf07a

Browse files
committed
JS: Add flow steps through iteration callback
1 parent e7bf485 commit a3cf07a

File tree

5 files changed

+106
-0
lines changed

5 files changed

+106
-0
lines changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,27 @@ private class PromiseTaintStep extends TaintTracking::AdditionalTaintStep {
224224
pred = source and succ = this
225225
}
226226
}
227+
228+
/**
229+
* A flow step propagating the exception thrown from a callback to a method whose name coincides
230+
* a built-in Array iteration method, such as `forEach` or `map`.
231+
*/
232+
private class IteratorExceptionStep extends DataFlow::MethodCallNode, DataFlow::AdditionalFlowStep {
233+
IteratorExceptionStep() {
234+
exists(string name | name = getMethodName() |
235+
name = "forEach" or
236+
name = "each" or
237+
name = "map" or
238+
name = "filter" or
239+
name = "some" or
240+
name = "every" or
241+
name = "fold" or
242+
name = "reduce"
243+
)
244+
}
245+
246+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
247+
pred = getAnArgument().(DataFlow::FunctionNode).getExceptionalReturn() and
248+
succ = this.getExceptionalReturn()
249+
}
250+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,13 @@ class InvokeNode extends DataFlow::SourceNode {
141141
* likely to be imprecise or incomplete.
142142
*/
143143
predicate isUncertain() { isImprecise() or isIncomplete() }
144+
145+
/**
146+
* Gets the data flow node representing an exception thrown from this invocation.
147+
*/
148+
DataFlow::ExceptionalInvocationReturnNode getExceptionalReturn() {
149+
DataFlow::exceptionalInvocationReturnNode(result, asExpr())
150+
}
144151
}
145152

146153
/** Auxiliary module used to cache a few related predicates together. */
@@ -356,6 +363,13 @@ class FunctionNode extends DataFlow::ValueNode, DataFlow::SourceNode {
356363
* To get the data flow node for `this` in an arrow function, consider using `getThisBinder().getReceiver()`.
357364
*/
358365
ThisNode getReceiver() { result.getBinder() = this }
366+
367+
/**
368+
* Gets the data flow node representing an exception thrown from this function.
369+
*/
370+
DataFlow::ExceptionalFunctionReturnNode getExceptionalReturn() {
371+
DataFlow::exceptionalFunctionReturnNode(result, astNode)
372+
}
359373
}
360374

361375
/** A data flow node corresponding to an object literal expression. */

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,46 @@ module LodashUnderscore {
360360
name = "eachRight" or
361361
name = "first"
362362
}
363+
364+
/**
365+
* A data flow step propagating an exception thrown from a callback to a Lodash/Underscore function.
366+
*/
367+
private class ExceptionStep extends DataFlow::CallNode, DataFlow::AdditionalFlowStep {
368+
ExceptionStep() {
369+
exists(string name |
370+
this = member(name).getACall()
371+
|
372+
// Collection methods
373+
name = "countBy" or
374+
name = "each" or
375+
name = "eachRight" or
376+
name = "forEach" or
377+
name = "forEachRight" or
378+
name = "every" or
379+
name = "filter" or
380+
name = "groupBy" or
381+
name = "orderBy" or
382+
name = "partition" or
383+
name = "reduce" or
384+
name = "reduceRight" or
385+
name = "some" or
386+
name = "sortBy" or
387+
388+
// Array methods
389+
name = "dropRightWhile" or
390+
name = "dropWhile" or
391+
name = "sortedIndexBy" or
392+
name = "sortedUniqBy" or
393+
name = "takeRightWhile" or
394+
name = "takeWhile"
395+
)
396+
}
397+
398+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
399+
pred = getAnArgument().(DataFlow::FunctionNode).getExceptionalReturn() and
400+
succ = this.getExceptionalReturn()
401+
}
402+
}
363403
}
364404

365405
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
| exceptions.js:115:21:115:28 | source() | exceptions.js:121:10:121:10 | e |
4040
| exceptions.js:144:9:144:16 | source() | exceptions.js:129:10:129:10 | e |
4141
| exceptions.js:144:9:144:16 | source() | exceptions.js:132:8:132:27 | returnThrownSource() |
42+
| exceptions.js:150:13:150:20 | source() | exceptions.js:153:10:153:10 | e |
43+
| exceptions.js:158:13:158:20 | source() | exceptions.js:161:10:161:10 | e |
4244
| indexOf.js:4:11:4:18 | source() | indexOf.js:9:10:9:10 | x |
4345
| partialCalls.js:4:17:4:24 | source() | partialCalls.js:17:14:17:14 | x |
4446
| partialCalls.js:4:17:4:24 | source() | partialCalls.js:20:14:20:14 | y |

javascript/ql/test/library-tests/TaintTracking/exceptions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,30 @@ function throwSource() {
144144
throw source();
145145
}
146146

147+
function throwThoughLibrary(xs) {
148+
try {
149+
xs.forEach(function() {
150+
throw source();
151+
})
152+
} catch (e) {
153+
sink(e); // NOT OK
154+
}
155+
156+
try {
157+
_.takeWhile(xs, function() {
158+
throw source();
159+
})
160+
} catch (e) {
161+
sink(e); // NOT OK
162+
}
163+
164+
try {
165+
window.addEventListener("message", function(e) {
166+
throw source();
167+
})
168+
} catch (e) {
169+
sink(e); // OK - doesn't catch exception from event listener
170+
}
171+
}
172+
147173
// semmle-extractor-options: --experimental

0 commit comments

Comments
 (0)