JS: Add flow summaries/steps for promises and async/await

This commit is contained in:
Asger F
2023-10-13 11:25:39 +02:00
parent 4319b07798
commit 5054c43b18
5 changed files with 454 additions and 7 deletions

View File

@@ -6,7 +6,9 @@ import javascript
private import dataflow.internal.StepSummary
/**
* A definition of a `Promise` object.
* A call to the `Promise` constructor, such as `new Promise((resolve, reject) => { ... })`.
*
* This includes calls to the built-in `Promise` constructor as well as promise implementations from known libraries, such as `bluebird`.
*/
abstract class PromiseDefinition extends DataFlow::SourceNode {
/** Gets the executor function of this promise object. */
@@ -196,6 +198,8 @@ module Promises {
override string getAProperty() { result = [valueProp(), errorProp()] }
}
predicate promiseConstructorRef = getAPromiseObject/0;
}
/**
@@ -267,7 +271,7 @@ private import semmle.javascript.dataflow.internal.PreCallGraphStep
* These steps are for `await p`, `new Promise()`, `Promise.resolve()`,
* `Promise.then()`, `Promise.catch()`, and `Promise.finally()`.
*/
private class PromiseStep extends PreCallGraphStep {
private class PromiseStep extends LegacyPreCallGraphStep {
override predicate loadStep(DataFlow::Node obj, DataFlow::Node element, string prop) {
PromiseFlow::loadStep(obj, element, prop)
}
@@ -459,7 +463,7 @@ module PromiseFlow {
}
}
private class PromiseTaintStep extends TaintTracking::SharedTaintStep {
private class PromiseTaintStep extends TaintTracking::LegacyTaintStep {
override predicate promiseStep(DataFlow::Node pred, DataFlow::Node succ) {
// from `x` to `new Promise((res, rej) => res(x))`
pred = succ.(PromiseDefinition).getResolveParameter().getACall().getArgument(0)
@@ -530,7 +534,7 @@ private module AsyncReturnSteps {
/**
* A data-flow step for ordinary and exceptional returns from async functions.
*/
private class AsyncReturn extends PreCallGraphStep {
private class AsyncReturn extends LegacyPreCallGraphStep {
override predicate storeStep(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) {
exists(DataFlow::FunctionNode f | f.getFunction().isAsync() |
// ordinary return
@@ -548,7 +552,7 @@ private module AsyncReturnSteps {
/**
* A data-flow step for ordinary return from an async function in a taint configuration.
*/
private class AsyncTaintReturn extends TaintTracking::SharedTaintStep {
private class AsyncTaintReturn extends TaintTracking::LegacyTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(Function f |
f.isAsync() and
@@ -665,7 +669,7 @@ private module ClosurePromise {
/**
* Taint steps through closure promise methods.
*/
private class ClosurePromiseTaintStep extends TaintTracking::SharedTaintStep {
private class ClosurePromiseTaintStep extends TaintTracking::LegacyTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
// static methods in goog.Promise
exists(DataFlow::CallNode call, string name |
@@ -699,7 +703,7 @@ private module DynamicImportSteps {
* let Foo = await import('./foo');
* ```
*/
class DynamicImportStep extends PreCallGraphStep {
class DynamicImportStep extends LegacyPreCallGraphStep {
override predicate storeStep(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) {
exists(DynamicImportExpr imprt |
pred = imprt.getImportedModule().getAnExportedValue("default") and

View File

@@ -1,2 +1,4 @@
private import AmbiguousCoreMethods
private import Arrays2
private import AsyncAwait
private import Promises2

View File

@@ -0,0 +1,104 @@
/**
* Contains flow steps to model flow through `async` functions and the `await` operator.
*/
private import javascript
private import semmle.javascript.dataflow.internal.DataFlowNode
private import semmle.javascript.dataflow.internal.AdditionalFlowInternal
private import semmle.javascript.dataflow.internal.DataFlowPrivate
/**
* Steps modelling flow in an `async` function.
*
* Note about promise-coercion and flattening:
* - `await` preserves non-promise values, e.g. `await "foo"` is just `"foo"`.
* - `return` preserves existing promise values, and boxes other values in a promise.
*
* We rely on `expectsContent` and `clearsContent` to handle coercion/flattening without risk of creating a nested promise object.
*
* The following is a brief overview of the steps we generate:
* ```js
* async function foo() {
* await x; // x --- READ[promise-value] ---> await x
* await x; // x --- VALUE -----------------> await x (has clearsContent)
* await x; // x --- READ[promise-error] ---> exception target
*
* return x; // x --- VALUE --> return node (has expectsContent)
* return x; // x --- VALUE --> synthetic node (clearsContent) --- STORE[promise-value] --> return node
*
* // exceptional return node --> STORE[promise-error] --> return node
* }
* ```
*/
class AsyncAwait extends AdditionalFlowInternal {
override predicate needsSynthesizedNode(AstNode node, string tag, DataFlowCallable container) {
// We synthesize a clearsContent node to contain the values that need to be boxed in a promise before returning
node.(Function).isAsync() and
container.asSourceCallable() = node and
tag = "async-raw-return"
}
override predicate clearsContent(DataFlow::Node node, DataFlow::ContentSet contents) {
node = getSynthesizedNode(_, "async-raw-return") and
contents = DataFlow::ContentSet::promiseFilter()
or
// The result of 'await' cannot be a promise. This is needed for the local flow step into 'await'
node.asExpr() instanceof AwaitExpr and
contents = DataFlow::ContentSet::promiseFilter()
}
override predicate expectsContent(DataFlow::Node node, DataFlow::ContentSet contents) {
// The final return value must be a promise. This is needed for the local flow step into the return node.
exists(Function f |
f.isAsync() and
node = TFunctionReturnNode(f) and
contents = DataFlow::ContentSet::promiseFilter()
)
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(AwaitExpr await |
// Allow non-promise values to propagate through await.
pred = await.getOperand().flow() and
succ = await.flow() // clears promise-content
)
or
exists(Function f |
// To avoid creating a nested promise, flow to two different nodes which only permit promises/non-promises respectively
f.isAsync() and
pred = f.getAReturnedExpr().flow()
|
succ = getSynthesizedNode(f, "async-raw-return") // clears promise-content
or
succ = TFunctionReturnNode(f) // expects promise-content
)
}
override predicate readStep(
DataFlow::Node pred, DataFlow::ContentSet contents, DataFlow::Node succ
) {
exists(AwaitExpr await | pred = await.getOperand().flow() |
contents = DataFlow::ContentSet::promiseValue() and
succ = await.flow()
or
contents = DataFlow::ContentSet::promiseError() and
succ = await.getExceptionTarget()
)
}
override predicate storeStep(
DataFlow::Node pred, DataFlow::ContentSet contents, DataFlow::Node succ
) {
exists(Function f | f.isAsync() |
// Box returned non-promise values in a promise
pred = getSynthesizedNode(f, "async-raw-return") and
contents = DataFlow::ContentSet::promiseValue() and
succ = TFunctionReturnNode(f)
or
// Store thrown exceptions in promise-error
pred = TExceptionalFunctionReturnNode(f) and
contents = DataFlow::ContentSet::promiseError() and
succ = TFunctionReturnNode(f)
)
}
}

View File

@@ -32,3 +32,20 @@ abstract class FunctionalPackageSummary extends SummarizedCallable {
result = API::moduleImport(this.getAPackageName()).getAnInvocation()
}
}
/**
* Gets a content from a set of contents that together represent all valid array indices.
*
* This can be used to generate flow summaries that should preserve precise array indices,
* in cases where `WithArrayElement` is not sufficient.
*/
string getAnArrayContent() {
// Values stored at a known, small index
result = "ArrayElement[" + getAPreciseArrayIndex() + "!]"
or
// Values stored at a known, but large index
result = "ArrayElement[" + (getMaxPreciseArrayIndex() + 1) + "..]"
or
// Values stored at an unknown index
result = "ArrayElement[?]"
}

View File

@@ -0,0 +1,320 @@
/**
* Contains flow summaries and steps modelling flow through `Promise` objects.
*/
private import javascript
private import semmle.javascript.dataflow.FlowSummary
private import FlowSummaryUtil
private DataFlow::SourceNode promiseConstructorRef() {
result = Promises::promiseConstructorRef()
or
result = DataFlow::moduleImport("bluebird")
or
result = DataFlow::moduleMember(["q", "kew", "bluebird"], "Promise") // note: bluebird.Promise == bluebird
or
result = Closure::moduleImport("goog.Promise")
}
//
// Note that the 'Awaited' token has a special interpretation.
// See a write-up here: https://github.com/github/codeql-javascript-team/issues/423
//
private class PromiseConstructor extends SummarizedCallable {
PromiseConstructor() { this = "new Promise()" }
override DataFlow::InvokeNode getACallSimple() {
// Disabled for now. The field-flow branch limit will be negatively affected by having
// calls to multiple variants of `new Promise()`.
none()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
// TODO: when FlowSummaryImpl.qll supports these summaries, remove the workaround in PromiseConstructorWorkaround
// resolve(value)
input = "Argument[0].Parameter[0].Argument[0]" and output = "ReturnValue.Awaited"
or
// reject(value)
input = "Argument[0].Parameter[1].Argument[0]" and output = "ReturnValue.Awaited[error]"
or
// throw from executor
input = "Argument[0].ReturnValue[exception]" and output = "ReturnValue.Awaited[error]"
)
}
}
/**
* A workaround to the `PromiseConstructor`, to be used until FlowSummaryImpl.qll has sufficient support
* for callbacks.
*/
module PromiseConstructorWorkaround {
class ResolveSummary extends SummarizedCallable {
ResolveSummary() { this = "new Promise() resolve callback" }
override DataFlow::InvokeNode getACallSimple() {
result =
promiseConstructorRef().getAnInstantiation().getCallback(0).getParameter(0).getACall()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]" and
output = "Argument[function].Member[resolve-value]"
}
}
class RejectCallback extends SummarizedCallable {
RejectCallback() { this = "new Promise() reject callback" }
override DataFlow::InvokeNode getACallSimple() {
result =
promiseConstructorRef().getAnInstantiation().getCallback(0).getParameter(1).getACall()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]" and
output = "Argument[function].Member[reject-value]"
}
}
class ConstructorSummary extends SummarizedCallable {
ConstructorSummary() { this = "new Promise() workaround" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAnInstantiation()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0].Parameter[0].Member[resolve-value]" and
output = "ReturnValue.Awaited"
or
input = "Argument[0].Parameter[1].Member[reject-value]" and
output = "ReturnValue.Awaited[error]"
or
input = "Argument[0].ReturnValue[exception]" and
output = "ReturnValue.Awaited[error]"
)
}
}
}
private class PromiseThen2Arguments extends SummarizedCallable {
PromiseThen2Arguments() { this = "Promise#then() with 2 arguments" }
override InstanceCall getACallSimple() {
result.getMethodName() = "then" and
result.getNumArgument() = 2
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0,1].ReturnValue" and output = "ReturnValue.Awaited"
or
input = "Argument[0,1].ReturnValue[exception]" and output = "ReturnValue.Awaited[error]"
or
input = "Argument[this].Awaited[value]" and output = "Argument[0].Parameter[0]"
or
input = "Argument[this].Awaited[error]" and output = "Argument[1].Parameter[0]"
)
}
}
private class PromiseThen1Argument extends SummarizedCallable {
PromiseThen1Argument() { this = "Promise#then() with 1 argument" }
override InstanceCall getACallSimple() {
result.getMethodName() = "then" and
result.getNumArgument() = 1
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0].ReturnValue" and output = "ReturnValue.Awaited"
or
input = "Argument[0].ReturnValue[exception]" and output = "ReturnValue.Awaited[error]"
or
input = "Argument[this].Awaited[value]" and output = "Argument[0].Parameter[0]"
or
input = "Argument[this].WithAwaited[error]" and output = "ReturnValue"
)
}
}
private class PromiseCatch extends SummarizedCallable {
PromiseCatch() { this = "Promise#catch()" }
override InstanceCall getACallSimple() { result.getMethodName() = "catch" }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0].ReturnValue" and output = "ReturnValue.Awaited"
or
input = "Argument[0].ReturnValue[exception]" and output = "ReturnValue.Awaited[error]"
or
input = "Argument[this].Awaited[value]" and output = "ReturnValue.Awaited[value]"
or
input = "Argument[this].Awaited[error]" and output = "Argument[0].Parameter[0]"
)
}
}
private class PromiseFinally extends SummarizedCallable {
PromiseFinally() { this = "Promise#finally()" }
override InstanceCall getACallSimple() { result.getMethodName() = "finally" }
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0].ReturnValue.Awaited[error]" and output = "ReturnValue.Awaited[error]"
or
input = "Argument[0].ReturnValue[exception]" and output = "ReturnValue.Awaited[error]"
or
input = "Argument[this].WithAwaited[value,error]" and output = "ReturnValue"
)
}
}
private class PromiseResolve extends SummarizedCallable {
PromiseResolve() { this = "Promise.resolve()" }
override InstanceCall getACallSimple() {
result = promiseConstructorRef().getAMemberCall("resolve")
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]" and
output = "ReturnValue.Awaited"
}
}
private class PromiseReject extends SummarizedCallable {
PromiseReject() { this = "Promise.reject()" }
override InstanceCall getACallSimple() {
result = promiseConstructorRef().getAMemberCall("reject")
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]" and
output = "ReturnValue.Awaited[error]"
}
}
private class PromiseAll extends SummarizedCallable {
PromiseAll() { this = "Promise.all()" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAMemberCall("all")
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
exists(string content | content = getAnArrayContent() |
input = "Argument[0]." + content + ".Awaited" and
output = "ReturnValue.Awaited[value]." + content
)
or
preservesValue = true and
input = "Argument[0].ArrayElement.WithAwaited[error]" and
output = "ReturnValue"
}
}
private class PromiseAnyLike extends SummarizedCallable {
PromiseAnyLike() { this = "Promise.any() or Promise.race()" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAMemberCall(["any", "race", "firstFulfilled"])
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0].ArrayElement" and
output = "ReturnValue.Awaited"
}
}
private class PromiseAllSettled extends SummarizedCallable {
PromiseAllSettled() { this = "Promise.allSettled()" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAMemberCall("allSettled")
or
result = DataFlow::moduleImport("promise.allsettled").getACall()
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
exists(string content | content = getAnArrayContent() |
input = "Argument[0]." + content + ".Awaited" and
output = "ReturnValue.Awaited[value]." + content + ".Member[value]"
or
input = "Argument[0]." + content + ".Awaited[error]" and
output = "ReturnValue.Awaited[value]." + content + ".Member[reason]"
)
}
}
private class BluebirdMapSeries extends SummarizedCallable {
BluebirdMapSeries() { this = "bluebird.mapSeries" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAMemberCall("mapSeries")
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
input = "Argument[0].Awaited.ArrayElement.Awaited" and
output = "Argument[1].Parameter[0]"
or
input = "Argument[0].Awaited.ArrayElement.WithAwaited[error]" and
output = "ReturnValue"
or
input = "Argument[0].WithAwaited[error]" and
output = "ReturnValue"
or
input = "Argument[1].ReturnValue.Awaited" and
output = "ReturnValue.Awaited.ArrayElement"
or
input = "Argument[1].ReturnValue.WithAwaited[error]" and
output = "ReturnValue"
)
}
}
/**
* - `Promise.withResolvers`, a method pending standardization,
* - `goog.Closure.withResolver()` (non-plural spelling)
* - `bluebird.Promise.defer()`
*/
private class PromiseWithResolversLike extends SummarizedCallable {
PromiseWithResolversLike() { this = "Promise.withResolvers()" }
override DataFlow::InvokeNode getACallSimple() {
result = promiseConstructorRef().getAMemberCall(["withResolver", "withResolvers", "defer"])
}
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
preservesValue = true and
(
// TODO: not currently supported by FlowSummaryImpl.qll
input = "ReturnValue.Member[resolve].Argument[0]" and
output = "ReturnValue.Member[promise].Awaited"
or
input = "ReturnValue.Member[reject].Argument[0]" and
output = "ReturnValue.Member[promise].Awaited[error]"
)
}
}