mirror of
https://github.com/github/codeql.git
synced 2026-04-28 02:05:14 +02:00
JS: add model of async package
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
|
||||
|
||||
216
javascript/ql/src/semmle/javascript/frameworks/AsyncPackage.qll
Normal file
216
javascript/ql/src/semmle/javascript/frameworks/AsyncPackage.qll
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user