JS: add model of async package

This commit is contained in:
Asger F
2018-10-11 15:10:37 +01:00
parent 786377d8dc
commit b40fa3845f
9 changed files with 365 additions and 0 deletions

View File

@@ -52,6 +52,7 @@ import semmle.javascript.dataflow.DataFlow
import semmle.javascript.dataflow.TaintTracking
import semmle.javascript.dataflow.TypeInference
import semmle.javascript.frameworks.AngularJS
import semmle.javascript.frameworks.AsyncPackage
import semmle.javascript.frameworks.AWS
import semmle.javascript.frameworks.Azure
import semmle.javascript.frameworks.Babel

View File

@@ -287,6 +287,16 @@ class FunctionNode extends DataFlow::ValueNode, DataFlow::DefaultSourceNode {
result = getParameter(_)
}
/** Gets the number of parameters declared on this function. */
int getNumParameter() {
result = count(astNode.getAParameter())
}
/** Holds if the last parameter of this function is a rest parameter. */
predicate hasRestParameter() {
astNode.hasRestParameter()
}
/** Gets the name of this function, if it has one. */
string getName() { result = astNode.getName() }

View File

@@ -0,0 +1,216 @@
/**
* Provides classes for working with [async](https://www.npmjs.com/package/async).
*/
import javascript
module AsyncPackage {
/**
* Gets a reference the given member of the `async` or `async-es` package.
*/
DataFlow::SourceNode member(string name) {
result = DataFlow::moduleMember("async", name) or
result = DataFlow::moduleMember("async-es", name)
}
/**
* Gets a reference to the given member or one of its `Limit` or `Series` variants.
*
* For example, `memberVariant("map")` finds references to `map`, `mapLimit`, and `mapSeries`.
*/
DataFlow::SourceNode memberVariant(string name) {
exists (string suffix | result = member(name + suffix) |
suffix = "" or suffix = "Limit" or suffix = "Series")
}
/**
* A call to `async.waterfall`.
*/
class Waterfall extends DataFlow::InvokeNode {
Waterfall() {
this = member("waterfall").getACall()
}
/**
* Gets the array of tasks, if it can be found.
*/
DataFlow::ArrayCreationNode getTaskArray() {
result.flowsTo(getArgument(0))
}
/**
* Gets the callback to invoke after all the last task in the array completes.
*/
DataFlow::FunctionNode getFinalCallback() {
result.flowsTo(getArgument(1))
}
/**
* Gets the `n`th task, if it can be found.
*/
DataFlow::FunctionNode getTask(int n) {
result.flowsTo(getTaskArray().getElement(n))
}
/**
* Gets the number of tasks.
*/
int getNumTasks() {
result = strictcount(getTaskArray().getAnElement())
}
}
/**
* Gets the last parameter declared by the given function, unless that's a rest parameter.
*/
private DataFlow::ParameterNode getLastParameter(DataFlow::FunctionNode function) {
not function.hasRestParameter() and
result = function.getParameter(function.getNumParameter() - 1)
}
/**
* An invocation of the callback in a waterfall task, passing arguments to the next waterfall task.
*
* Such a callback has the form `callback(err, result1, result2, ...)`. The error is propagated
* to the first parameter of the final callback, while `result1, result2, ...` are propagated to
* the paramters of the following task.
*/
private class WaterfallNextTaskCall extends DataFlow::AdditionalPartialInvokeNode {
Waterfall waterfall;
int n;
WaterfallNextTaskCall() {
this = getLastParameter(waterfall.getTask(n)).getACall()
}
override predicate isPartialArgument(DataFlow::Node callback, DataFlow::Node argument, int index) {
// Pass results to next task
index >= 0 and
argument = getArgument(index + 1) and
callback = waterfall.getTask(n + 1)
or
// For the last task, pass results to the final callback
index >= 1 and
n = waterfall.getNumTasks() - 1 and
argument = getArgument(index) and
callback = waterfall.getFinalCallback()
or
// Always pass error to the final callback
index = 0 and
argument = getArgument(0) and
callback = waterfall.getFinalCallback()
}
}
/**
* A call that iterates over a collection with asynchronous operations.
*/
class IterationCall extends DataFlow::InvokeNode {
string name;
IterationCall() {
this = memberVariant(name).getACall() and
(
name = "concat" or
name = "detect" or
name = "each" or
name = "eachOf" or
name = "forEach" or
name = "forEachOf" or
name = "every" or
name = "filter" or
name = "groupBy" or
name = "map" or
name = "mapValues" or
name = "reduce" or
name = "reduceRight" or
name = "reject" or
name = "some" or
name = "sortBy" or
name = "transform"
)
}
/**
* Gets the name of the iteration call, without the `Limit` or `Series` suffix.
*/
string getName() { result = name }
/**
* Gets the node holding the collection being iterated over.
*/
DataFlow::Node getCollection() {
result = getArgument(0)
}
/**
* Gets the node holding the function being called for each element in the collection.
*/
DataFlow::Node getIteratee() {
result = getArgument(getNumArgument() - 2)
}
/**
* Gets the node holding the function being invoked after iteration is complete.
*/
DataFlow::Node getFinalCallback() {
result = getArgument(getNumArgument() - 1)
}
}
/**
* A taint step from the collection into the iterator callback of an iteration call.
*
* For example: `data -> item` in `async.each(data, (item, cb) => {})`.
*/
private class IterationInputTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists (DataFlow::FunctionNode iteratee |
iteratee = getIteratee() and // Require a closure to avoid spurious call/return mismatch.
pred = getCollection() and
succ = iteratee.getParameter(0))
}
}
/**
* A taint step from the return value of an iterator callback to the result of the iteration
* call.
*
* For example: `item + taint()` -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
*/
private class IterationOutputTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
IterationOutputTaintStep() {
name = "concat" or
name = "map" or
name = "reduce" or
name = "reduceRight"
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists (DataFlow::FunctionNode iteratee, DataFlow::FunctionNode final, int i |
iteratee = getIteratee().getALocalSource() and
final = getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = getLastParameter(iteratee).getACall().getArgument(i) and
succ = final.getParameter(i))
}
}
/**
* A taint step from the input of an iteration call, directly to its output.
*
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
*/
private class IterationPreserveTaintStep extends TaintTracking::AdditionalTaintStep, IterationCall {
IterationPreserveTaintStep() {
name = "sortBy"
// We don't currently include `filter` and `reject` as they could act as sanitizers.
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists (DataFlow::FunctionNode final |
final = getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = getCollection() and
succ = final.getParameter(1))
}
}
}

View File

@@ -0,0 +1,8 @@
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item |
| map.js:10:13:10:20 | source() | map.js:12:14:12:17 | item |
| map.js:20:19:20:26 | source() | map.js:23:27:23:32 | result |
| map.js:26:13:26:20 | source() | map.js:28:27:28:32 | result |
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result |
| waterfall.js:7:30:7:37 | source() | waterfall.js:10:12:10:16 | taint |
| waterfall.js:7:30:7:37 | source() | waterfall.js:19:10:19:14 | taint |
| waterfall.js:27:18:27:25 | source() | waterfall.js:38:10:38:12 | err |

View File

@@ -0,0 +1,23 @@
import javascript
DataFlow::CallNode getACall(string name) {
result.getCalleeName() = name
}
class BasicConfig extends TaintTracking::Configuration {
BasicConfig() { this = "BasicConfig" }
override
predicate isSource(DataFlow::Node node) {
node = getACall("source")
}
override
predicate isSink(DataFlow::Node node) {
node = getACall("sink").getAnArgument()
}
}
from BasicConfig cfg, DataFlow::Node src, DataFlow::Node sink
where cfg.hasFlow(src, sink)
select src, sink

View File

@@ -0,0 +1,20 @@
let async_ = require('async');
function source() {
return 'TAINT'
}
function sink(x) {
console.log(x)
}
async_.each(
[1, source(), 2],
function (item, callback) {
sink(item); // NOT OK
callback(null, 'Hello ' + item);
},
function (err, result) {
sink(err); // OK
sink(result); // OK - 'each' does not propagate return value
}
)

View File

@@ -0,0 +1,34 @@
let async_ = require('async');
function source() {
return 'TAINT'
}
function sink(x) {
console.log(x)
}
async_.map([source()],
(item, cb) => {
sink(item), // NOT OK
cb(null, 'safe');
},
(err, result) => sink(result) // OK
);
async_.map(['safe'],
(item, cb) => {
let src = source();
cb(null, src);
},
(err, result) => sink(result) // NOT OK
);
async_.map([source()],
(item, cb) => cb(null, item.substring(1)),
(err, result) => sink(result) // NOT OK
);
async_.map(['safe'],
(item, cb) => cb(null, item),
(err, result) => sink(result) // OK
);

View File

@@ -0,0 +1,12 @@
let async_ = require('async');
function source() {
return 'TAINT'
}
function sink(x) {
console.log(x)
}
async_.sortBy(['zz', source()],
(x, cb) => cb(x.length),
(err, result) => sink(result)); // NOT OK

View File

@@ -0,0 +1,41 @@
let async_ = require('async');
var source, sink, somethingWrong;
async_.waterfall([
function(callback) {
callback(null, 'safe', source());
},
function(safe, taint, callback) {
sink(taint); // NOT OK
sink(safe); // OK
callback(null, taint, safe);
},
function(taint, safe, callback) {
callback(null, taint, safe);
}
],
function finalCallback(err, taint, safe) {
sink(taint); // NOT OK
sink(safe); // OK
}
);
async_.waterfall([
function(callback) {
if (somethingWrong()) {
callback(source());
} else {
callback(null, 'safe');
}
},
function(safe, callback) {
sink(safe); // OK
callback(null, safe);
}
],
function(err, safe) {
sink(err); // NOT OK
sink(safe); // OK
}
);