Merge pull request #2855 from asger-semmle/js/returned-partial-call

Approved by esbena
This commit is contained in:
semmle-qlci
2020-02-24 21:37:41 +00:00
committed by GitHub
14 changed files with 204 additions and 21 deletions

View File

@@ -71,6 +71,7 @@
private import javascript
private import internal.FlowSteps
private import internal.AccessPaths
private import internal.CallGraphs
/**
* A data flow tracking configuration for finding inter-procedural paths from
@@ -620,10 +621,11 @@ private predicate exploratoryFlowStep(
isAdditionalStoreStep(pred, succ, _, cfg) or
isAdditionalLoadStep(pred, succ, _, cfg) or
isAdditionalLoadStoreStep(pred, succ, _, cfg) or
// the following two disjuncts taken together over-approximate flow through
// the following three disjuncts taken together over-approximate flow through
// higher-order calls
callback(pred, succ) or
succ = pred.(DataFlow::FunctionNode).getAParameter()
succ = pred.(DataFlow::FunctionNode).getAParameter() or
exploratoryBoundInvokeStep(pred, succ)
}
/**
@@ -751,7 +753,7 @@ private predicate flowThroughCall(
) {
exists(Function f, DataFlow::ValueNode ret |
ret.asExpr() = f.getAReturnedExpr() and
calls(output, f) and // Do not consider partial calls
(calls(output, f) or callsBound(output, f, _)) and // Do not consider partial calls
reachableFromInput(f, output, input, ret, cfg, summary) and
not isBarrierEdge(cfg, ret, output) and
not isLabeledBarrierEdge(cfg, ret, output, summary.getEndLabel()) and
@@ -761,7 +763,7 @@ private predicate flowThroughCall(
exists(Function f, DataFlow::Node invk, DataFlow::Node ret |
DataFlow::exceptionalFunctionReturnNode(ret, f) and
DataFlow::exceptionalInvocationReturnNode(output, invk.asExpr()) and
calls(invk, f) and
(calls(invk, f) or callsBound(invk, f, _)) and
reachableFromInput(f, invk, input, ret, cfg, summary) and
not isBarrierEdge(cfg, ret, output) and
not isLabeledBarrierEdge(cfg, ret, output, summary.getEndLabel()) and
@@ -1032,6 +1034,13 @@ private predicate flowIntoHigherOrderCall(
succ = cb.getParameter(i) and
summary = oldSummary.append(PathSummary::call())
)
or
exists(DataFlow::SourceNode cb, DataFlow::FunctionNode f, int i, int boundArgs, PathSummary oldSummary |
higherOrderCall(pred, cb, i, cfg, oldSummary) and
cb = CallGraph::getABoundFunctionReference(f, boundArgs, false) and
succ = f.getParameter(boundArgs + i) and
summary = oldSummary.append(PathSummary::call())
)
}
/**

View File

@@ -147,7 +147,7 @@ module DataFlow {
final FunctionNode getABoundFunctionValue(int boundArgs) {
result = getAFunctionValue() and boundArgs = 0
or
CallGraph::getABoundFunctionReference(result, boundArgs).flowsTo(this)
CallGraph::getABoundFunctionReference(result, boundArgs, _).flowsTo(this)
}
/**

View File

@@ -74,7 +74,7 @@ module CallGraph {
*/
pragma[nomagic]
private
DataFlow::SourceNode getABoundFunctionReference(DataFlow::FunctionNode function, int boundArgs, DataFlow::TypeTracker t) {
DataFlow::SourceNode getABoundFunctionReferenceAux(DataFlow::FunctionNode function, int boundArgs, DataFlow::TypeTracker t) {
exists(DataFlow::PartialInvokeNode partial, DataFlow::Node callback |
result = partial.getBoundFunction(callback, boundArgs) and
getAFunctionReference(function, 0, t.continue()).flowsTo(callback)
@@ -90,7 +90,7 @@ module CallGraph {
private
DataFlow::SourceNode getABoundFunctionReferenceAux(DataFlow::FunctionNode function, int boundArgs, DataFlow::TypeTracker t, DataFlow::StepSummary summary) {
exists(DataFlow::SourceNode prev |
prev = getABoundFunctionReference(function, boundArgs, t) and
prev = getABoundFunctionReferenceAux(function, boundArgs, t) and
DataFlow::StepSummary::step(prev, result, summary)
)
}
@@ -100,8 +100,12 @@ module CallGraph {
* with `function` as the underlying function.
*/
cached
DataFlow::SourceNode getABoundFunctionReference(DataFlow::FunctionNode function, int boundArgs) {
result = getABoundFunctionReference(function, boundArgs, DataFlow::TypeTracker::end())
DataFlow::SourceNode getABoundFunctionReference(DataFlow::FunctionNode function, int boundArgs, boolean contextDependent) {
exists(DataFlow::TypeTracker t |
result = getABoundFunctionReferenceAux(function, boundArgs, t) and
t.end() and
contextDependent = t.hasCall()
)
}
/**

View File

@@ -6,6 +6,7 @@
import javascript
import semmle.javascript.dataflow.Configuration
import semmle.javascript.dataflow.internal.CallGraphs
/**
* Holds if flow should be tracked through properties of `obj`.
@@ -91,6 +92,32 @@ private module CachedSteps {
cached
predicate calls(DataFlow::InvokeNode invk, Function f) { f = invk.getACallee(0) }
/**
* Holds if `invk` may invoke a bound version of `f` with `boundArgs` already bound.
*
* The receiver is assumed to be bound as well, and should not propagate into `f`.
*
* Does not hold for context-dependent call sites, such as callback invocations.
*/
cached
predicate callsBound(DataFlow::InvokeNode invk, Function f, int boundArgs) {
CallGraph::getABoundFunctionReference(f.flow(), boundArgs, false).flowsTo(invk.getCalleeNode())
}
/**
* Holds if `pred` may flow to `succ` through an invocation of a bound function.
*
* Should only be used for graph pruning, as the edge may lead to spurious flow.
*/
cached
predicate exploratoryBoundInvokeStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::InvokeNode invk, DataFlow::FunctionNode f, int i, int boundArgs |
CallGraph::getABoundFunctionReference(f, boundArgs, _).flowsTo(invk.getCalleeNode()) and
pred = invk.getArgument(i) and
succ = f.getParameter(i + boundArgs)
)
}
/**
* Holds if `invk` may invoke `f` indirectly through the given `callback` argument.
*
@@ -141,6 +168,14 @@ private module CachedSteps {
partiallyCalls(invk, callback, f) and
parm = DataFlow::thisNode(f)
)
or
exists(int boundArgs, int i, Parameter p |
callsBound(invk, f, boundArgs) and
f.getParameter(boundArgs + i) = p and
not p.isRestParameter() and
arg = invk.getArgument(i) and
parm = DataFlow::parameterNode(p)
)
}
/**
@@ -158,7 +193,7 @@ private module CachedSteps {
*/
cached
predicate returnStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Function f | calls(succ, f) |
exists(Function f | calls(succ, f) or callsBound(succ, f, _) |
returnExpr(f, pred, _)
or
succ instanceof DataFlow::NewNode and
@@ -167,8 +202,11 @@ private module CachedSteps {
or
exists(InvokeExpr invoke, Function fun |
DataFlow::exceptionalFunctionReturnNode(pred, fun) and
DataFlow::exceptionalInvocationReturnNode(succ, invoke) and
DataFlow::exceptionalInvocationReturnNode(succ, invoke)
|
calls(invoke.flow(), fun)
or
callsBound(invoke.flow(), fun, _)
)
}
@@ -464,4 +502,3 @@ module PathSummary {
*/
PathSummary return() { exists(FlowLabel lbl | result = MkPathSummary(true, false, lbl, lbl)) }
}

View File

@@ -1,5 +1,5 @@
spuriousCallee
missingCallee
| constructor-field.ts:40:5:40:14 | f3.build() | constructor-field.ts:13:3:13:12 | build() {} |
| constructor-field.ts:71:1:71:11 | bf3.build() | constructor-field.ts:13:3:13:12 | build() {} |
| constructor-field.ts:40:5:40:14 | f3.build() | constructor-field.ts:13:3:13:12 | build() {} | -1 |
| constructor-field.ts:71:1:71:11 | bf3.build() | constructor-field.ts:13:3:13:12 | build() {} | -1 |
badAnnotation

View File

@@ -33,16 +33,37 @@ class AnnotatedCall extends InvokeExpr {
string getCallTargetName() { result = calls }
AnnotatedFunction getAnExpectedCallee() { result.getCalleeName() = getCallTargetName() }
int getBoundArgs() {
result = getAnnotation(this, "boundArgs").toInt()
}
int getBoundArgsOrMinusOne() {
result = getBoundArgs()
or
not exists(getBoundArgs()) and
result = -1
}
}
query predicate spuriousCallee(AnnotatedCall call, AnnotatedFunction target) {
FlowSteps::calls(call.flow(), target) and
not target = call.getAnExpectedCallee()
predicate callEdge(AnnotatedCall call, AnnotatedFunction target, int boundArgs) {
FlowSteps::calls(call.flow(), target) and boundArgs = -1
or
FlowSteps::callsBound(call.flow(), target, boundArgs)
}
query predicate missingCallee(AnnotatedCall call, AnnotatedFunction target) {
not FlowSteps::calls(call.flow(), target) and
target = call.getAnExpectedCallee()
query predicate spuriousCallee(AnnotatedCall call, AnnotatedFunction target, int boundArgs) {
callEdge(call, target, boundArgs) and
not (
target = call.getAnExpectedCallee() and
boundArgs = call.getBoundArgsOrMinusOne()
)
}
query predicate missingCallee(AnnotatedCall call, AnnotatedFunction target, int boundArgs) {
not callEdge(call, target, boundArgs) and
target = call.getAnExpectedCallee() and
boundArgs = call.getBoundArgsOrMinusOne()
}
query predicate badAnnotation(string name) {

View File

@@ -0,0 +1,31 @@
var url = require('url')
var http = require('http')
function Mount () {}
/** name:mount.serve */
Mount.prototype.serve = function (x) {
}
function makeMount() {
var m = new Mount()
return m.serve.bind(m);
}
function makeMount2(x) {
var m = new Mount()
return m.serve.bind(m, x);
}
var mount = makeMount()
var mount2 = makeMount2(null);
http.createServer(function (req, res) {
/** calls:mount.serve */
/** boundArgs:0 */
mount(req, res)
/** calls:mount.serve */
/** boundArgs:1 */
mount2(res)
});

View File

@@ -22,6 +22,10 @@
| partial.js:5:15:5:24 | "tainted1" | partial.js:15:15:15:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:21:15:21:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:27:15:27:15 | x |
| partial.js:6:15:6:24 | "tainted2" | partial.js:10:15:10:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:16:15:16:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:22:15:22:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:28:15:28:15 | y |
| promises.js:2:16:2:24 | "tainted" | promises.js:7:16:7:18 | val |
| promises.js:2:16:2:24 | "tainted" | promises.js:38:32:38:32 | v |
| promises.js:11:22:11:31 | "resolved" | promises.js:19:20:19:20 | v |

View File

@@ -23,6 +23,10 @@
| partial.js:5:15:5:24 | "tainted1" | partial.js:15:15:15:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:21:15:21:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:27:15:27:15 | x |
| partial.js:6:15:6:24 | "tainted2" | partial.js:10:15:10:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:16:15:16:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:22:15:22:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:28:15:28:15 | y |
| promises.js:2:16:2:24 | "tainted" | promises.js:7:16:7:18 | val |
| promises.js:2:16:2:24 | "tainted" | promises.js:38:32:38:32 | v |
| promises.js:11:22:11:31 | "resolved" | promises.js:19:20:19:20 | v |

View File

@@ -27,6 +27,10 @@
| partial.js:5:15:5:24 | "tainted1" | partial.js:15:15:15:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:21:15:21:15 | x |
| partial.js:5:15:5:24 | "tainted1" | partial.js:27:15:27:15 | x |
| partial.js:6:15:6:24 | "tainted2" | partial.js:10:15:10:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:16:15:16:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:22:15:22:15 | y |
| partial.js:6:15:6:24 | "tainted2" | partial.js:28:15:28:15 | y |
| promises.js:2:16:2:24 | "tainted" | promises.js:7:16:7:18 | val |
| promises.js:2:16:2:24 | "tainted" | promises.js:38:32:38:32 | v |
| promises.js:11:22:11:31 | "resolved" | promises.js:19:20:19:20 | v |

View File

@@ -15,6 +15,12 @@ typeInferenceMismatch
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:13:10:13:10 | x |
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:19:10:19:10 | x |
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:22:10:22:10 | x |
| bound-function.js:12:12:12:19 | source() | bound-function.js:4:10:4:10 | y |
| bound-function.js:14:6:14:13 | source() | bound-function.js:4:10:4:10 | y |
| bound-function.js:22:8:22:15 | source() | bound-function.js:25:10:25:10 | y |
| bound-function.js:45:10:45:17 | source() | bound-function.js:45:6:45:18 | id3(source()) |
| bound-function.js:49:12:49:19 | source() | bound-function.js:54:6:54:14 | source0() |
| bound-function.js:49:12:49:19 | source() | bound-function.js:55:6:55:14 | source1() |
| callbacks.js:4:6:4:13 | source() | callbacks.js:34:27:34:27 | x |
| callbacks.js:4:6:4:13 | source() | callbacks.js:35:27:35:27 | x |
| callbacks.js:5:6:5:13 | source() | callbacks.js:34:27:34:27 | x |

View File

@@ -6,6 +6,12 @@
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:13:10:13:10 | x |
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:19:10:19:10 | x |
| booleanOps.js:2:11:2:18 | source() | booleanOps.js:22:10:22:10 | x |
| bound-function.js:12:12:12:19 | source() | bound-function.js:4:10:4:10 | y |
| bound-function.js:14:6:14:13 | source() | bound-function.js:4:10:4:10 | y |
| bound-function.js:22:8:22:15 | source() | bound-function.js:25:10:25:10 | y |
| bound-function.js:45:10:45:17 | source() | bound-function.js:45:6:45:18 | id3(source()) |
| bound-function.js:49:12:49:19 | source() | bound-function.js:54:6:54:14 | source0() |
| bound-function.js:49:12:49:19 | source() | bound-function.js:55:6:55:14 | source1() |
| callbacks.js:4:6:4:13 | source() | callbacks.js:34:27:34:27 | x |
| callbacks.js:4:6:4:13 | source() | callbacks.js:35:27:35:27 | x |
| callbacks.js:5:6:5:13 | source() | callbacks.js:34:27:34:27 | x |

View File

@@ -0,0 +1,55 @@
import * as dummy from 'dummy';
function foo(x, y) {
sink(y);
}
let foo0 = foo.bind(null);
let foo1 = foo.bind(null, null);
let foo2 = foo.bind(null, null, null);
foo0(source(), null); // OK
foo0(null, source()); // NOT OK
foo1(source()); // NOT OK
foo1(null, source()); // OK
foo2(source()); // OK
foo2(null, source()); // OK
function takesCallback(cb) {
cb(source()); // NOT OK
}
function callback(x, y) {
sink(y);
}
takesCallback(callback.bind(null, null));
function id(x) {
return x;
}
let sourceGetter = id.bind(null, source());
let constGetter = id.bind(null, 'safe');
sink(sourceGetter()); // NOT OK - but not flagged
sink(constGetter()); // OK
function id2(x, y) {
return y;
}
let id3 = id2.bind(null, null);
sink(id3(source())); // NOT OK
sink(id3('safe')); // OK
function getSource() {
return source();
}
let source0 = getSource.bind(null);
let source1 = getSource.bind(null, null);
sink(source0()); // NOT OK
sink(source1()); // NOT OK