Files
codeql/javascript/ql/lib/semmle/javascript/internal/flow_summaries/Promises.qll
2025-09-01 15:17:53 +02:00

371 lines
12 KiB
Plaintext

/**
* Contains flow summaries and steps modeling flow through `Promise` objects.
*/
private import javascript
private import semmle.javascript.dataflow.FlowSummary
private import semmle.javascript.dataflow.TypeTracking
private import FlowSummaryUtil
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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(string input, string output, boolean preservesValue) {
preservesValue = true and
input = "Argument[0]" and
output = "ReturnValue.Awaited[error]"
}
}
/**
* A call to `Promise.all()`.
*/
class PromiseAllCall extends DataFlow::CallNode {
PromiseAllCall() { this = promiseConstructorRef().getAMemberCall("all") }
/** Gets the source of the input array */
DataFlow::ArrayCreationNode getInputArray() { result = this.getArgument(0).getALocalSource() }
/** Gets the `n`th element of the input array */
DataFlow::Node getNthInput(int n) { result = this.getInputArray().getElement(n) }
/** Gets a reference to the output array. */
DataFlow::SourceNode getOutputArray() {
exists(AwaitExpr await |
this.flowsToExpr(await.getOperand()) and
result = await.flow()
)
or
result = this.getAMethodCall("then").getCallback(0).getParameter(0)
}
/** Gets the `n`th output */
DataFlow::SourceNode getNthOutput(int n) {
exists(string prop |
result = this.getOutputArray().getAPropertyRead(prop) and
n = prop.toInt()
)
}
}
/**
* Helps type-tracking simple uses of `Promise.all()` such as `const [a, b] = await Promise.all([x, y])`.
*
* Due to limited access path depth, type tracking can't track things that are in a promise and an array
* at once. This generates a step directly from the input array to the output array.
*/
private class PromiseAllStep extends SharedTypeTrackingStep {
override predicate loadStep(DataFlow::Node node1, DataFlow::Node node2, string prop) {
exists(PromiseAllCall call, int n |
node1 = call.getNthInput(n) and
node2 = call.getNthOutput(n) and
prop = Promises::valueProp()
)
}
}
private class PromiseAll extends SummarizedCallable {
PromiseAll() { this = "Promise.all()" }
override DataFlow::InvokeNode getACallSimple() { result instanceof PromiseAllCall }
override predicate propagatesFlow(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"
or
preservesValue = false and
input = "Argument[0]" 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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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 propagatesFlow(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]"
)
}
}