Merge pull request #1308 from asger-semmle/exceptional-flow

JS: Add flow through exceptions
This commit is contained in:
Esben Sparre Andreasen
2019-05-17 08:33:44 +02:00
committed by GitHub
11 changed files with 435 additions and 8 deletions

View File

@@ -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()
}
}

View File

@@ -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())
)
}

View File

@@ -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`:

View File

@@ -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. */

View File

@@ -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.

View File

@@ -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, _)
)
}

View File

@@ -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)
)
}
/**

View File

@@ -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()
}
}
}
/**

View File

@@ -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 |

View 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