mirror of
https://github.com/github/codeql.git
synced 2026-04-29 18:55:14 +02:00
Merge pull request #1308 from asger-semmle/exceptional-flow
JS: Add flow through exceptions
This commit is contained in:
@@ -224,3 +224,27 @@ private class PromiseTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
pred = source and succ = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow step propagating the exception thrown from a callback to a method whose name coincides
|
||||
* a built-in Array iteration method, such as `forEach` or `map`.
|
||||
*/
|
||||
private class IteratorExceptionStep extends DataFlow::MethodCallNode, DataFlow::AdditionalFlowStep {
|
||||
IteratorExceptionStep() {
|
||||
exists(string name | name = getMethodName() |
|
||||
name = "forEach" or
|
||||
name = "each" or
|
||||
name = "map" or
|
||||
name = "filter" or
|
||||
name = "some" or
|
||||
name = "every" or
|
||||
name = "fold" or
|
||||
name = "reduce"
|
||||
)
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
pred = getAnArgument().(DataFlow::FunctionNode).getExceptionalReturn() and
|
||||
succ = this.getExceptionalReturn()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,8 @@ abstract class Configuration extends string {
|
||||
isBarrierGuard(guard) and
|
||||
guard.blocks(node)
|
||||
)
|
||||
or
|
||||
none() // relax type inference to account for overriding
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,14 +601,23 @@ private predicate appendStep(
|
||||
* configuration `cfg`, possibly through callees.
|
||||
*/
|
||||
private predicate flowThroughCall(
|
||||
DataFlow::Node input, DataFlow::Node invk, DataFlow::Configuration cfg, PathSummary summary
|
||||
DataFlow::Node input, DataFlow::Node output, DataFlow::Configuration cfg, PathSummary summary
|
||||
) {
|
||||
exists(Function f, DataFlow::ValueNode ret |
|
||||
ret.asExpr() = f.getAReturnedExpr() and
|
||||
calls(invk, f) and // Do not consider partial calls
|
||||
calls(output, f) and // Do not consider partial calls
|
||||
reachableFromInput(f, output, input, ret, cfg, summary) and
|
||||
not cfg.isBarrier(ret, output) and
|
||||
not cfg.isLabeledBarrier(output, summary.getEndLabel())
|
||||
)
|
||||
or
|
||||
exists(Function f, DataFlow::Node invk, DataFlow::Node ret |
|
||||
DataFlow::exceptionalFunctionReturnNode(ret, f) and
|
||||
DataFlow::exceptionalInvocationReturnNode(output, invk.asExpr()) and
|
||||
calls(invk, f) and
|
||||
reachableFromInput(f, invk, input, ret, cfg, summary) and
|
||||
not cfg.isBarrier(ret, invk) and
|
||||
not cfg.isLabeledBarrier(invk, summary.getEndLabel())
|
||||
not cfg.isBarrier(ret, output) and
|
||||
not cfg.isLabeledBarrier(output, summary.getEndLabel())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ module DataFlow {
|
||||
TDestructuredModuleImportNode(ImportDeclaration decl) {
|
||||
exists(decl.getASpecifier().getImportedName())
|
||||
} or
|
||||
THtmlAttributeNode(HTML::Attribute attr)
|
||||
THtmlAttributeNode(HTML::Attribute attr) or
|
||||
TExceptionalFunctionReturnNode(Function f) or
|
||||
TExceptionalInvocationReturnNode(InvokeExpr e)
|
||||
|
||||
/**
|
||||
* A node in the data flow graph.
|
||||
@@ -773,6 +775,50 @@ module DataFlow {
|
||||
HTML::Attribute getAttribute() { result = attr }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node representing the exceptions thrown by a function.
|
||||
*/
|
||||
class ExceptionalFunctionReturnNode extends DataFlow::Node, TExceptionalFunctionReturnNode {
|
||||
Function function;
|
||||
|
||||
ExceptionalFunctionReturnNode() { this = TExceptionalFunctionReturnNode(function) }
|
||||
|
||||
override string toString() { result = "exceptional return of " + function.describe() }
|
||||
|
||||
override predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
function.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the function corresponding to this exceptional return node.
|
||||
*/
|
||||
Function getFunction() { result = function }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node representing the exceptions thrown by the callee of an invocation.
|
||||
*/
|
||||
class ExceptionalInvocationReturnNode extends DataFlow::Node, TExceptionalInvocationReturnNode {
|
||||
InvokeExpr invoke;
|
||||
|
||||
ExceptionalInvocationReturnNode() { this = TExceptionalInvocationReturnNode(invoke) }
|
||||
|
||||
override string toString() { result = "exceptional return of " + invoke.toString() }
|
||||
|
||||
override predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
invoke.getLocation().hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the invocation corresponding to this exceptional return node.
|
||||
*/
|
||||
DataFlow::InvokeNode getInvocation() { result = invoke.flow() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides classes representing various kinds of calls.
|
||||
*
|
||||
@@ -977,6 +1023,20 @@ module DataFlow {
|
||||
result = TDestructuredModuleImportNode(imprt)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Use `ExceptionalInvocationReturnNode` instead.
|
||||
*/
|
||||
predicate exceptionalInvocationReturnNode(DataFlow::Node nd, InvokeExpr invocation) {
|
||||
nd = TExceptionalInvocationReturnNode(invocation)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Use `ExceptionalFunctionReturnNode` instead.
|
||||
*/
|
||||
predicate exceptionalFunctionReturnNode(DataFlow::Node nd, Function function) {
|
||||
nd = TExceptionalFunctionReturnNode(function)
|
||||
}
|
||||
|
||||
/**
|
||||
* A classification of flows that are not modeled, or only modeled incompletely, by
|
||||
* `DataFlowNode`:
|
||||
|
||||
@@ -141,6 +141,13 @@ class InvokeNode extends DataFlow::SourceNode {
|
||||
* likely to be imprecise or incomplete.
|
||||
*/
|
||||
predicate isUncertain() { isImprecise() or isIncomplete() }
|
||||
|
||||
/**
|
||||
* Gets the data flow node representing an exception thrown from this invocation.
|
||||
*/
|
||||
DataFlow::ExceptionalInvocationReturnNode getExceptionalReturn() {
|
||||
DataFlow::exceptionalInvocationReturnNode(result, asExpr())
|
||||
}
|
||||
}
|
||||
|
||||
/** Auxiliary module used to cache a few related predicates together. */
|
||||
@@ -356,6 +363,13 @@ class FunctionNode extends DataFlow::ValueNode, DataFlow::SourceNode {
|
||||
* To get the data flow node for `this` in an arrow function, consider using `getThisBinder().getReceiver()`.
|
||||
*/
|
||||
ThisNode getReceiver() { result.getBinder() = this }
|
||||
|
||||
/**
|
||||
* Gets the data flow node representing an exception thrown from this function.
|
||||
*/
|
||||
DataFlow::ExceptionalFunctionReturnNode getExceptionalReturn() {
|
||||
DataFlow::exceptionalFunctionReturnNode(result, astNode)
|
||||
}
|
||||
}
|
||||
|
||||
/** A data flow node corresponding to an object literal expression. */
|
||||
|
||||
@@ -579,6 +579,30 @@ module TaintTracking {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint step through an exception constructor, such as `x` to `new Error(x)`.
|
||||
*/
|
||||
class ErrorConstructorTaintStep extends AdditionalTaintStep, DataFlow::InvokeNode {
|
||||
ErrorConstructorTaintStep() {
|
||||
exists(string name |
|
||||
this = DataFlow::globalVarRef(name).getAnInvocation()
|
||||
|
|
||||
name = "Error" or
|
||||
name = "EvalError" or
|
||||
name = "RangeError" or
|
||||
name = "ReferenceError" or
|
||||
name = "SyntaxError" or
|
||||
name = "TypeError" or
|
||||
name = "URIError"
|
||||
)
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
pred = getArgument(0) and
|
||||
succ = this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A conditional checking a tainted string against a regular expression, which is
|
||||
* considered to be a sanitizer for all configurations.
|
||||
|
||||
@@ -55,6 +55,8 @@ private module NodeTracking {
|
||||
pred = succ.getAPredecessor()
|
||||
or
|
||||
any(DataFlow::AdditionalFlowStep afs).step(pred, succ)
|
||||
or
|
||||
localExceptionStep(pred, succ)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,9 +155,16 @@ private module NodeTracking {
|
||||
* which is either an argument or a definition captured by the function, flows,
|
||||
* possibly through callees.
|
||||
*/
|
||||
private predicate flowThroughCall(DataFlow::Node input, DataFlow::Node invk) {
|
||||
private predicate flowThroughCall(DataFlow::Node input, DataFlow::Node output) {
|
||||
exists(Function f, DataFlow::ValueNode ret |
|
||||
ret.asExpr() = f.getAReturnedExpr() and
|
||||
reachableFromInput(f, output, input, ret, _)
|
||||
)
|
||||
or
|
||||
exists(Function f, DataFlow::Node invk, DataFlow::Node ret |
|
||||
DataFlow::exceptionalFunctionReturnNode(ret, f) and
|
||||
DataFlow::exceptionalInvocationReturnNode(output, invk.asExpr()) and
|
||||
calls(invk, f) and
|
||||
reachableFromInput(f, invk, input, ret, _)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,35 @@ predicate localFlowStep(
|
||||
)
|
||||
or
|
||||
configuration.isAdditionalFlowStep(pred, succ, predlbl, succlbl)
|
||||
or
|
||||
localExceptionStep(pred, succ) and
|
||||
predlbl = succlbl
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if an exception thrown from `pred` can propagate locally to `succ`.
|
||||
*/
|
||||
predicate localExceptionStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(Expr expr |
|
||||
expr = any(ThrowStmt throw).getExpr() and
|
||||
pred = expr.flow()
|
||||
or
|
||||
DataFlow::exceptionalInvocationReturnNode(pred, expr)
|
||||
|
|
||||
// Propagate out of enclosing function.
|
||||
not exists(getEnclosingTryStmt(expr.getEnclosingStmt())) and
|
||||
exists(Function f |
|
||||
f = expr.getEnclosingFunction() and
|
||||
DataFlow::exceptionalFunctionReturnNode(succ, f)
|
||||
)
|
||||
or
|
||||
// Propagate to enclosing try/catch.
|
||||
// To avoid false flow, we only propagate to an unguarded catch clause.
|
||||
exists(TryStmt try |
|
||||
try = getEnclosingTryStmt(expr.getEnclosingStmt()) and
|
||||
DataFlow::parameterNode(succ, try.getCatchClause().getAParameter())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,8 +150,23 @@ private module CachedSteps {
|
||||
predicate callStep(DataFlow::Node pred, DataFlow::Node succ) { argumentPassing(_, pred, _, succ) }
|
||||
|
||||
/**
|
||||
* Holds if there is a flow step from `pred` to `succ` through returning
|
||||
* from a function call or the receiver flowing out of a constructor call.
|
||||
* Gets the `try` statement containing `stmt` without crossing function boundaries
|
||||
* or other `try ` statements.
|
||||
*/
|
||||
cached
|
||||
TryStmt getEnclosingTryStmt(Stmt stmt) {
|
||||
result.getBody() = stmt
|
||||
or
|
||||
not stmt instanceof Function and
|
||||
not stmt = any(TryStmt try).getBody() and
|
||||
result = getEnclosingTryStmt(stmt.getParentStmt())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a flow step from `pred` to `succ` through:
|
||||
* - returning a value from a function call, or
|
||||
* - throwing an exception out of a function call, or
|
||||
* - the receiver flowing out of a constructor call.
|
||||
*/
|
||||
cached
|
||||
predicate returnStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
@@ -132,6 +176,12 @@ private module CachedSteps {
|
||||
succ instanceof DataFlow::NewNode and
|
||||
DataFlow::thisNode(pred, f)
|
||||
)
|
||||
or
|
||||
exists(InvokeExpr invoke, Function fun |
|
||||
DataFlow::exceptionalFunctionReturnNode(pred, fun) and
|
||||
DataFlow::exceptionalInvocationReturnNode(succ, invoke) and
|
||||
calls(invoke.flow(), fun)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -360,6 +360,47 @@ module LodashUnderscore {
|
||||
name = "eachRight" or
|
||||
name = "first"
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow step propagating an exception thrown from a callback to a Lodash/Underscore function.
|
||||
*/
|
||||
private class ExceptionStep extends DataFlow::CallNode, DataFlow::AdditionalFlowStep {
|
||||
ExceptionStep() {
|
||||
exists(string name |
|
||||
this = member(name).getACall()
|
||||
|
|
||||
// Members ending with By, With, or While indicate that they are a variant of
|
||||
// another function that takes a callback.
|
||||
name.matches("%By") or
|
||||
name.matches("%With") or
|
||||
name.matches("%While") or
|
||||
|
||||
// Other members that don't fit the above pattern.
|
||||
name = "each" or
|
||||
name = "eachRight" or
|
||||
name = "every" or
|
||||
name = "filter" or
|
||||
name = "find" or
|
||||
name = "findLast" or
|
||||
name = "flatMap" or
|
||||
name = "flatMapDeep" or
|
||||
name = "flatMapDepth" or
|
||||
name = "forEach" or
|
||||
name = "forEachRight" or
|
||||
name = "partition" or
|
||||
name = "reduce" or
|
||||
name = "reduceRight" or
|
||||
name = "replace" or
|
||||
name = "some" or
|
||||
name = "transform"
|
||||
)
|
||||
}
|
||||
|
||||
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
pred = getAnArgument().(DataFlow::FunctionNode).getExceptionalReturn() and
|
||||
succ = this.getExceptionalReturn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,25 @@
|
||||
| constructor-calls.js:10:16:10:23 | source() | constructor-calls.js:30:8:30:19 | d_safe.taint |
|
||||
| constructor-calls.js:14:15:14:22 | source() | constructor-calls.js:17:8:17:14 | c.param |
|
||||
| constructor-calls.js:14:15:14:22 | source() | constructor-calls.js:25:8:25:14 | d.param |
|
||||
| exceptions.js:3:15:3:22 | source() | exceptions.js:5:10:5:10 | e |
|
||||
| exceptions.js:21:17:21:24 | source() | exceptions.js:23:10:23:10 | e |
|
||||
| exceptions.js:21:17:21:24 | source() | exceptions.js:24:10:24:21 | e.toString() |
|
||||
| exceptions.js:21:17:21:24 | source() | exceptions.js:25:10:25:18 | e.message |
|
||||
| exceptions.js:21:17:21:24 | source() | exceptions.js:26:10:26:19 | e.fileName |
|
||||
| exceptions.js:48:16:48:23 | source() | exceptions.js:50:10:50:10 | e |
|
||||
| exceptions.js:59:24:59:31 | source() | exceptions.js:61:12:61:12 | e |
|
||||
| exceptions.js:88:6:88:13 | source() | exceptions.js:11:10:11:10 | e |
|
||||
| exceptions.js:88:6:88:13 | source() | exceptions.js:32:10:32:10 | e |
|
||||
| exceptions.js:88:6:88:13 | source() | exceptions.js:33:10:33:21 | e.toString() |
|
||||
| exceptions.js:88:6:88:13 | source() | exceptions.js:34:10:34:18 | e.message |
|
||||
| exceptions.js:88:6:88:13 | source() | exceptions.js:35:10:35:19 | e.fileName |
|
||||
| exceptions.js:93:11:93:18 | source() | exceptions.js:95:10:95:10 | e |
|
||||
| exceptions.js:100:13:100:20 | source() | exceptions.js:102:12:102:12 | e |
|
||||
| exceptions.js:115:21:115:28 | source() | exceptions.js:121:10:121:10 | e |
|
||||
| exceptions.js:144:9:144:16 | source() | exceptions.js:129:10:129:10 | e |
|
||||
| exceptions.js:144:9:144:16 | source() | exceptions.js:132:8:132:27 | returnThrownSource() |
|
||||
| exceptions.js:150:13:150:20 | source() | exceptions.js:153:10:153:10 | e |
|
||||
| exceptions.js:158:13:158:20 | source() | exceptions.js:161:10:161:10 | e |
|
||||
| indexOf.js:4:11:4:18 | source() | indexOf.js:9:10:9:10 | x |
|
||||
| partialCalls.js:4:17:4:24 | source() | partialCalls.js:17:14:17:14 | x |
|
||||
| partialCalls.js:4:17:4:24 | source() | partialCalls.js:20:14:20:14 | y |
|
||||
|
||||
173
javascript/ql/test/library-tests/TaintTracking/exceptions.js
Normal file
173
javascript/ql/test/library-tests/TaintTracking/exceptions.js
Normal file
@@ -0,0 +1,173 @@
|
||||
function test(unsafe, safe) {
|
||||
try {
|
||||
throwRaw2(source());
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
try {
|
||||
throwRaw2(unsafe);
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
try {
|
||||
throwRaw2(safe);
|
||||
} catch (e) {
|
||||
sink(e); // OK
|
||||
}
|
||||
|
||||
try {
|
||||
throwError2(source());
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
sink(e.toString()); // NOT OK
|
||||
sink(e.message); // NOT OK
|
||||
sink(e.fileName); // OK - but flagged anyway
|
||||
}
|
||||
|
||||
try {
|
||||
throwError2(unsafe);
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
sink(e.toString()); // NOT OK
|
||||
sink(e.message); // NOT OK
|
||||
sink(e.fileName); // OK - but flagged anyway
|
||||
}
|
||||
|
||||
try {
|
||||
throwError2(safe);
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
sink(e.toString()); // NOT OK
|
||||
sink(e.message); // NOT OK
|
||||
sink(e.fileName); // OK - but flagged anyway
|
||||
}
|
||||
|
||||
try {
|
||||
throwAsync(source());
|
||||
} catch (e) {
|
||||
sink(e); // OK - but flagged anyway
|
||||
}
|
||||
|
||||
throwAsync(source()).catch(e => {
|
||||
sink(e); // NOT OK - but not flagged
|
||||
});
|
||||
|
||||
async function asyncTester() {
|
||||
try {
|
||||
await throwAsync(source());
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function throwRaw2(x) {
|
||||
throwRaw1(x);
|
||||
throwRaw1(x); // no single-call inlining
|
||||
}
|
||||
|
||||
function throwRaw1(x) {
|
||||
throw x;
|
||||
}
|
||||
|
||||
function throwError2(x) {
|
||||
throwError1(x);
|
||||
throwError1(x); // no single-call inlining
|
||||
}
|
||||
|
||||
function throwError1(x) {
|
||||
throw new Error(x);
|
||||
}
|
||||
|
||||
async function throwAsync(x) {
|
||||
throw x; // doesn't actually throw - returns failed promise
|
||||
}
|
||||
|
||||
test(source(), "hello");
|
||||
test("hey", "hello"); // no single-call inlining
|
||||
|
||||
function testNesting(x) {
|
||||
try {
|
||||
throw source();
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
throw source();
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
} catch (e) {
|
||||
sink(e); // OK - not caught by this catch
|
||||
}
|
||||
|
||||
try {
|
||||
if (x) {
|
||||
for (;x;) {
|
||||
while(x) {
|
||||
switch (x) {
|
||||
case 1:
|
||||
default:
|
||||
throw source();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
}
|
||||
|
||||
function testThrowSourceInCallee() {
|
||||
try {
|
||||
throwSource();
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
sink(returnThrownSource()); // NOT OK
|
||||
}
|
||||
|
||||
function returnThrownSource() {
|
||||
try {
|
||||
throwSource();
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
function throwSource() {
|
||||
throw source();
|
||||
}
|
||||
|
||||
function throwThoughLibrary(xs) {
|
||||
try {
|
||||
xs.forEach(function() {
|
||||
throw source();
|
||||
})
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
try {
|
||||
_.takeWhile(xs, function() {
|
||||
throw source();
|
||||
})
|
||||
} catch (e) {
|
||||
sink(e); // NOT OK
|
||||
}
|
||||
|
||||
try {
|
||||
window.addEventListener("message", function(e) {
|
||||
throw source();
|
||||
})
|
||||
} catch (e) {
|
||||
sink(e); // OK - doesn't catch exception from event listener
|
||||
}
|
||||
}
|
||||
|
||||
// semmle-extractor-options: --experimental
|
||||
Reference in New Issue
Block a user