JavaScript: add flow steps through partial function application

This commit is contained in:
Asger F
2018-09-05 14:49:30 +01:00
parent 7f56be6fe2
commit 269bbc9a1a
7 changed files with 147 additions and 0 deletions

View File

@@ -283,6 +283,18 @@ abstract class AdditionalSink extends DataFlow::Node {
abstract predicate isSinkFor(Configuration cfg);
}
/**
* An invocation that is modeled as a partial function application.
*
* This contributes additional argument-passing flow edges that should be added to all data flow configurations.
*/
abstract class AdditionalPartialInvokeNode extends DataFlow::InvokeNode {
/**
* Holds if `argument` is passed as argument `index` to the function in `callback`.
*/
abstract predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index);
}
/**
* Additional flow step to model flow from import specifiers into the SSA variable
* corresponding to the imported variable.
@@ -299,6 +311,37 @@ private class FlowStepThroughImport extends AdditionalFlowStep, DataFlow::ValueN
}
}
/**
* A partial call through the built-in `Function.prototype.bind`.
*/
private class BindPartialCall extends AdditionalPartialInvokeNode, DataFlow::MethodCallNode {
BindPartialCall() {
getMethodName() = "bind"
}
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getReceiver() and
argument = getArgument(index + 1)
}
}
/**
* A partial call through `_.partial` or a function with a similar interface.
*/
private class LibraryPartialCall extends AdditionalPartialInvokeNode {
LibraryPartialCall() {
this = LodashUnderscore::member("partial").getACall() or
this = DataFlow::moduleMember("ramda", "partial").getACall()
}
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
callback = getArgument(0) and
exists (DataFlow::ArrayLiteralNode array |
array = getArgument(1) and
argument = array.getElement(index))
}
}
/**
* Holds if there is a flow step from `pred` to `succ` described by `summary`
* under configuration `cfg`.
@@ -445,6 +488,7 @@ private predicate flowThroughCall(DataFlow::Node input, DataFlow::Node invk,
DataFlow::Configuration cfg, boolean valuePreserving) {
exists (Function f, DataFlow::ValueNode ret |
ret.asExpr() = f.getAReturnedExpr() and
calls(invk, f) and // Do not consider partial calls
reachableFromInput(f, invk, input, ret, cfg, PathSummary::level(valuePreserving))
)
}

View File

@@ -27,6 +27,21 @@ predicate calls(DataFlow::InvokeNode invk, Function f) {
f = invk.getACallee()
}
/**
* Holds if `invk` may invoke `f` indirectly through the given `callback` argument.
*
* This only holds for explicitly modeled partial calls.
*/
predicate partiallyCalls(DataFlow::AdditionalPartialInvokeNode invk, DataFlow::Node callback, Function f) {
invk.isPartialArgument(callback, _, _) and
exists (AbstractFunction callee | callee = callback.analyze().getAValue() |
if invk.isIndefinite("global") then
(f = callee.getFunction() and f.getFile() = invk.getFile())
else
f = callee.getFunction()
)
}
/**
* Holds if `f` captures the variable defined by `def` in `cap`.
*/
@@ -69,6 +84,11 @@ predicate argumentPassing(DataFlow::InvokeNode invk, DataFlow::ValueNode arg, Fu
f.getParameter(i) = parm and not parm.isRestParameter() and
arg = invk.getArgument(i)
)
or
exists (DataFlow::Node callback, int i |
invk.(DataFlow::AdditionalPartialInvokeNode).isPartialArgument(callback, arg, i) and
partiallyCalls(invk, callback, f) and
parm = f.getParameter(i) and not parm.isRestParameter())
}

View File

@@ -2,6 +2,10 @@
| etherpad.js:11:12:11:19 | response | Cross-site scripting vulnerability due to $@. | etherpad.js:9:16:9:30 | req.query.jsonp | user-provided value |
| formatting.js:6:14:6:47 | util.fo ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| formatting.js:7:14:7:53 | require ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| partial.js:10:14:10:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:13:42:13:48 | req.url | user-provided value |
| partial.js:19:14:19:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:22:52:22:58 | req.url | user-provided value |
| partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:31:48:31:54 | req.url | user-provided value |
| partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:40:43:40:49 | req.url | user-provided value |
| promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
| tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:6:9:6:9 | p | user-provided value |
| tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value |

View File

@@ -11,6 +11,18 @@ edges
| formatting.js:4:16:4:29 | req.query.evil | formatting.js:4:9:4:29 | evil |
| formatting.js:6:43:6:46 | evil | formatting.js:6:14:6:47 | util.fo ... , evil) |
| formatting.js:7:49:7:52 | evil | formatting.js:7:14:7:53 | require ... , evil) |
| partial.js:9:25:9:25 | x | partial.js:10:14:10:14 | x |
| partial.js:10:14:10:14 | x | partial.js:10:14:10:18 | x + y |
| partial.js:13:42:13:48 | req.url | partial.js:9:25:9:25 | x |
| partial.js:18:25:18:25 | x | partial.js:19:14:19:14 | x |
| partial.js:19:14:19:14 | x | partial.js:19:14:19:18 | x + y |
| partial.js:22:52:22:58 | req.url | partial.js:18:25:18:25 | x |
| partial.js:27:25:27:25 | x | partial.js:28:14:28:14 | x |
| partial.js:28:14:28:14 | x | partial.js:28:14:28:18 | x + y |
| partial.js:31:48:31:54 | req.url | partial.js:27:25:27:25 | x |
| partial.js:36:25:36:25 | x | partial.js:37:14:37:14 | x |
| partial.js:37:14:37:14 | x | partial.js:37:14:37:18 | x + y |
| partial.js:40:43:40:49 | req.url | partial.js:36:25:36:25 | x |
| promises.js:5:3:5:59 | new Pro ... .data)) | promises.js:6:11:6:11 | x |
| promises.js:5:44:5:57 | req.query.data | promises.js:5:3:5:59 | new Pro ... .data)) |
| promises.js:5:44:5:57 | req.query.data | promises.js:6:11:6:11 | x |
@@ -25,6 +37,10 @@ edges
| etherpad.js:11:12:11:19 | response | etherpad.js:9:16:9:30 | req.query.jsonp | etherpad.js:11:12:11:19 | response | Cross-site scripting vulnerability due to $@. | etherpad.js:9:16:9:30 | req.query.jsonp | user-provided value |
| formatting.js:6:14:6:47 | util.fo ... , evil) | formatting.js:4:16:4:29 | req.query.evil | formatting.js:6:14:6:47 | util.fo ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| formatting.js:7:14:7:53 | require ... , evil) | formatting.js:4:16:4:29 | req.query.evil | formatting.js:7:14:7:53 | require ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| partial.js:10:14:10:18 | x + y | partial.js:13:42:13:48 | req.url | partial.js:10:14:10:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:13:42:13:48 | req.url | user-provided value |
| partial.js:19:14:19:18 | x + y | partial.js:22:52:22:58 | req.url | partial.js:19:14:19:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:22:52:22:58 | req.url | user-provided value |
| partial.js:28:14:28:18 | x + y | partial.js:31:48:31:54 | req.url | partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:31:48:31:54 | req.url | user-provided value |
| partial.js:37:14:37:18 | x + y | partial.js:40:43:40:49 | req.url | partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:40:43:40:49 | req.url | user-provided value |
| promises.js:6:25:6:25 | x | promises.js:5:44:5:57 | req.query.data | promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
| promises.js:6:25:6:25 | x | promises.js:5:44:5:57 | req.query.data | promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
| tst2.js:7:12:7:12 | p | tst2.js:6:9:6:9 | p | tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:6:9:6:9 | p | user-provided value |

View File

@@ -1,6 +1,10 @@
| ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:8:33:8:45 | req.params.id | user-provided value |
| formatting.js:6:14:6:47 | util.fo ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| formatting.js:7:14:7:53 | require ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| partial.js:10:14:10:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:13:42:13:48 | req.url | user-provided value |
| partial.js:19:14:19:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:22:52:22:58 | req.url | user-provided value |
| partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:31:48:31:54 | req.url | user-provided value |
| partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:40:43:40:49 | req.url | user-provided value |
| promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
| tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:6:9:6:9 | p | user-provided value |
| tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value |

View File

@@ -5,6 +5,10 @@ WARNING: Predicate flowsFrom has been deprecated and may be removed in future (R
| ReflectedXss.js:8:14:8:45 | "Unknow ... rams.id | Cross-site scripting vulnerability due to $@. | ReflectedXss.js:8:33:8:45 | req.params.id | user-provided value |
| formatting.js:6:14:6:47 | util.fo ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| formatting.js:7:14:7:53 | require ... , evil) | Cross-site scripting vulnerability due to $@. | formatting.js:4:16:4:29 | req.query.evil | user-provided value |
| partial.js:10:14:10:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:13:42:13:48 | req.url | user-provided value |
| partial.js:19:14:19:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:22:52:22:58 | req.url | user-provided value |
| partial.js:28:14:28:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:31:48:31:54 | req.url | user-provided value |
| partial.js:37:14:37:18 | x + y | Cross-site scripting vulnerability due to $@. | partial.js:40:43:40:49 | req.url | user-provided value |
| promises.js:6:25:6:25 | x | Cross-site scripting vulnerability due to $@. | promises.js:5:44:5:57 | req.query.data | user-provided value |
| tst2.js:7:12:7:12 | p | Cross-site scripting vulnerability due to $@. | tst2.js:6:9:6:9 | p | user-provided value |
| tst2.js:8:12:8:12 | r | Cross-site scripting vulnerability due to $@. | tst2.js:6:12:6:15 | q: r | user-provided value |

View File

@@ -0,0 +1,55 @@
let express = require('express');
let underscore = require('underscore');
let lodash = require('lodash');
let R = require('ramda');
let app = express();
app.get("/some/path", (req, res) => {
function sendResponse(x, y) {
res.send(x + y); // NOT OK
}
let callback = sendResponse.bind(null, req.url);
[1, 2, 3].forEach(callback);
});
app.get("/underscore", (req, res) => {
function sendResponse(x, y) {
res.send(x + y); // NOT OK
}
let callback = underscore.partial(sendResponse, [req.url]);
[1, 2, 3].forEach(callback);
});
app.get("/lodash", (req, res) => {
function sendResponse(x, y) {
res.send(x + y); // NOT OK
}
let callback = lodash.partial(sendResponse, [req.url]);
[1, 2, 3].forEach(callback);
});
app.get("/ramda", (req, res) => {
function sendResponse(x, y) {
res.send(x + y); // NOT OK
}
let callback = R.partial(sendResponse, [req.url]);
[1, 2, 3].forEach(callback);
});
app.get("/return", (req, res) => {
function getFirst(x, y) {
return x;
}
let callback = getFirst.bind(null, req.url);
res.send(callback); // OK - the callback itself is not tainted
res.send(callback()); // NOT OK - but not currently detected
res.send(getFirst("Hello")); // OK - argument is not tainted from this call site
});