Merge pull request #19770 from trailofbits/VF/async-package-improvements

Improve data flow in the `async` package
This commit is contained in:
Asger F
2025-09-16 08:55:52 +02:00
committed by GitHub
5 changed files with 127 additions and 44 deletions

View File

@@ -94,6 +94,8 @@ private string encodeContentAux(ContentSet cs, string arg) {
cs = ContentSet::iteratorElement() and result = "IteratorElement"
or
cs = ContentSet::iteratorError() and result = "IteratorError"
or
cs = ContentSet::anyProperty() and result = "AnyMember"
)
or
cs = getPromiseContent(arg) and

View File

@@ -25,6 +25,18 @@ module AsyncPackage {
result = member(name + "Series")
}
/**
* Gets `Limit` or `Series` name variants for a given member name.
*
* For example, `memberNameVariant("map")` returns `map`, `mapLimit`, and `mapSeries`.
*/
bindingset[name]
private string memberNameVariant(string name) {
result = name or
result = name + "Limit" or
result = name + "Series"
}
/**
* A call to `async.waterfall`.
*/
@@ -101,22 +113,47 @@ module AsyncPackage {
*/
class IterationCall extends DataFlow::InvokeNode {
string name;
int iteratorCallbackIndex;
int finalCallbackIndex;
IterationCall() {
this = memberVariant(name).getACall() and
name =
[
"concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
"groupBy", "map", "mapValues", "reduce", "reduceRight", "reject", "some", "sortBy",
"transform"
]
(
(
name =
memberNameVariant([
"concat", "detect", "each", "eachOf", "forEach", "forEachOf", "every", "filter",
"groupBy", "map", "mapValues", "reject", "some", "sortBy",
]) and
if name.matches("%Limit")
then (
iteratorCallbackIndex = 2 and finalCallbackIndex = 3
) else (
iteratorCallbackIndex = 1 and finalCallbackIndex = 2
)
)
or
name = ["reduce", "reduceRight", "transform"] and
iteratorCallbackIndex = 2 and
finalCallbackIndex = 3
) and
this = member(name).getACall()
}
/**
* Gets the name of the iteration call, without the `Limit` or `Series` suffix.
* Gets the name of the iteration call
*/
string getName() { result = name }
/**
* Gets the iterator callback index
*/
int getIteratorCallbackIndex() { result = iteratorCallbackIndex }
/**
* Gets the final callback index
*/
int getFinalCallbackIndex() { result = finalCallbackIndex }
/**
* Gets the node holding the collection being iterated over.
*/
@@ -125,26 +162,33 @@ module AsyncPackage {
/**
* Gets the node holding the function being called for each element in the collection.
*/
DataFlow::Node getIteratorCallback() { result = this.getArgument(this.getNumArgument() - 2) }
DataFlow::FunctionNode getIteratorCallback() {
result = this.getCallback(iteratorCallbackIndex)
}
/**
* Gets the node holding the function being invoked after iteration is complete.
* Gets the node holding the function being invoked after iteration is complete. (may not exist)
*/
DataFlow::Node getFinalCallback() { result = this.getArgument(this.getNumArgument() - 1) }
DataFlow::FunctionNode getFinalCallback() { result = this.getCallback(finalCallbackIndex) }
}
/**
* 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::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::FunctionNode iteratee, IterationCall call |
iteratee = call.getIteratorCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = call.getCollection() and // TODO: needs a flow summary to ensure ArrayElement content is unfolded
succ = iteratee.getParameter(0)
)
private class IterationCallFlowSummary extends DataFlow::SummarizedCallable {
private int callbackArgIndex;
IterationCallFlowSummary() {
this = "async.IteratorCall(callbackArgIndex=" + callbackArgIndex + ")" and
callbackArgIndex in [1 .. 3]
}
override DataFlow::InvokeNode getACallSimple() {
result instanceof IterationCall and
result.(IterationCall).getIteratorCallbackIndex() = callbackArgIndex
}
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
output = "Argument[" + callbackArgIndex + "].Parameter[0]"
}
}
@@ -152,14 +196,14 @@ module AsyncPackage {
* 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) => {})`.
* For example: `item + taint() -> result` in `async.map(data, (item, cb) => cb(null, item + taint()), (err, result) => {})`.
*/
private class IterationOutputTaintStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(
DataFlow::FunctionNode iteratee, DataFlow::FunctionNode final, int i, IterationCall call
|
iteratee = call.getIteratorCallback().getALocalSource() and
iteratee = call.getIteratorCallback() and
final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = getLastParameter(iteratee).getACall().getArgument(i) and
succ = final.getParameter(i) and
@@ -175,14 +219,18 @@ module AsyncPackage {
*
* For example: `data -> result` in `async.sortBy(data, orderingFn, (err, result) => {})`.
*/
private class IterationPreserveTaintStep extends TaintTracking::SharedTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::FunctionNode final, IterationCall call |
final = call.getFinalCallback() and // Require a closure to avoid spurious call/return mismatch.
pred = call.getCollection() and
succ = final.getParameter(1) and
call.getName() = "sortBy"
)
private class IterationPreserveTaintStepFlowSummary extends DataFlow::SummarizedCallable {
IterationPreserveTaintStepFlowSummary() { this = "async.sortBy" }
override DataFlow::InvokeNode getACallSimple() {
result instanceof IterationCall and
result.(IterationCall).getName() = "sortBy"
}
override predicate propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = false and
input = "Argument[0]." + ["ArrayElement", "SetElement", "IteratorElement", "AnyMember"] and
output = "Argument[2].Parameter[1]"
}
}
}

View File

@@ -1,12 +1,24 @@
legacyDataFlowDifference
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with OLD data flow library |
| map.js:10:13:10:20 | source() | map.js:12:14:12:17 | item | only flow with OLD data flow library |
| map.js:26:13:26:20 | source() | map.js:28:27:28:32 | result | only flow with OLD data flow library |
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with OLD data flow library |
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item | only flow with NEW data flow library |
| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item | only flow with NEW data flow library |
| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result | only flow with NEW data flow library |
| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x | only flow with NEW data flow library |
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result | only flow with NEW data flow library |
#select
| map.js:20:19:20:26 | source() | map.js:23:27:23:32 | result |
| waterfall.js:8:30:8:37 | source() | waterfall.js:11:12:11:16 | taint |
| waterfall.js:8:30:8:37 | source() | waterfall.js:20:10:20:14 | taint |
| waterfall.js:28:18:28:25 | source() | waterfall.js:39:10:39:12 | err |
| waterfall.js:46:22:46:29 | source() | waterfall.js:49:12:49:16 | taint |
| waterfall.js:46:22:46:29 | source() | waterfall.js:55:10:55:14 | taint |
| each.js:11:9:11:16 | source() | each.js:13:12:13:15 | item |
| map.js:14:13:14:20 | source() | map.js:16:14:16:17 | item |
| map.js:24:19:24:26 | source() | map.js:27:27:27:32 | result |
| map.js:30:13:30:20 | source() | map.js:32:27:32:32 | result |
| map.js:40:13:40:20 | source() | map.js:11:10:11:10 | x |
| map.js:42:12:42:19 | source() | map.js:11:10:11:10 | x |
| map.js:44:16:44:23 | source() | map.js:11:10:11:10 | x |
| map.js:46:18:46:25 | source() | map.js:11:10:11:10 | x |
| sortBy.js:10:22:10:29 | source() | sortBy.js:12:27:12:32 | result |
| waterfall.js:16:30:16:37 | source() | waterfall.js:19:12:19:16 | taint |
| waterfall.js:16:30:16:37 | source() | waterfall.js:28:10:28:14 | taint |
| waterfall.js:36:18:36:25 | source() | waterfall.js:47:10:47:12 | err |
| waterfall.js:54:22:54:29 | source() | waterfall.js:57:12:57:16 | taint |
| waterfall.js:54:22:54:29 | source() | waterfall.js:63:10:63:14 | taint |

View File

@@ -7,6 +7,10 @@ function sink(x) {
console.log(x)
}
function call_sink(x) {
sink(x)
}
async_.map([source()],
(item, cb) => {
sink(item), // NOT OK
@@ -32,3 +36,12 @@ async_.map(['safe'],
(item, cb) => cb(null, item),
(err, result) => sink(result) // OK
);
async_.map([source()], call_sink) // NOT OK
async_.map(source().prop, call_sink) // NOT OK
async_.map({a: source()}, call_sink) // NOT OK
async_.mapLimit([source()], 1, call_sink) // NOT OK

View File

@@ -1,7 +1,15 @@
let async_ = require('async');
let waterfall = require('a-sync-waterfall');
var source, sink, somethingWrong;
function source() {
return 'TAINT'
}
function sink(x) {
console.log(x)
}
var somethingWrong;
async_.waterfall([
function(callback) {