Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---
* Added modeling for promisification libraries `@gar/promisify`, `es6-promisify`, `util.promisify`, `thenify-all`, `call-me-maybe`, `@google-cloud/promisify`, and `util-promisify`.
* Data flow is now tracked through promisified user-defined functions.
6 changes: 6 additions & 0 deletions javascript/ql/lib/ext/call-me-maybe.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
extensions:
- addsTo:
pack: codeql/javascript-all
extensible: summaryModel
data:
- ["call-me-maybe", "", "Argument[1]", "ReturnValue", "value"]
11 changes: 11 additions & 0 deletions javascript/ql/lib/semmle/javascript/ApiGraphs.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,17 @@ module API {
ref = awaited(call)
)
or
// Handle promisified object member access: promisify(obj).member should be treated as obj.member (promisified)
exists(
Promisify::PromisifyAllCall promisifiedObj, DataFlow::SourceNode originalObj,
string member
|
originalObj.flowsTo(promisifiedObj.getArgument(0)) and
use(base, originalObj) and
lbl = Label::member(member) and
ref = promisifiedObj.getAPropertyRead(member)
)
or
decoratorDualEdge(base, lbl, ref)
or
decoratorUseEdge(base, lbl, ref)
Expand Down
14 changes: 10 additions & 4 deletions javascript/ql/lib/semmle/javascript/Promises.qll
Original file line number Diff line number Diff line change
Expand Up @@ -727,8 +727,12 @@ module Promisify {
PromisifyAllCall() {
this =
[
DataFlow::moduleMember("bluebird", "promisifyAll"),
DataFlow::moduleImport(["util-promisifyall", "pify"])
DataFlow::moduleMember(["bluebird", "@google-cloud/promisify", "es6-promisify"],
"promisifyAll"),
DataFlow::moduleMember("thenify-all", "withCallback"),
DataFlow::moduleImport([
"util-promisifyall", "pify", "thenify-all", "@gar/promisify", "util.promisify-all"
])
].getACall()
}
}
Expand All @@ -741,11 +745,13 @@ module Promisify {
PromisifyCall() {
this = DataFlow::moduleImport(["util", "bluebird"]).getAMemberCall("promisify")
or
this = DataFlow::moduleImport(["pify", "util.promisify"]).getACall()
this = DataFlow::moduleImport(["pify", "util.promisify", "util-promisify"]).getACall()
or
this = DataFlow::moduleImport("thenify").getACall()
this = DataFlow::moduleImport(["thenify", "@gar/promisify", "es6-promisify"]).getACall()
or
this = DataFlow::moduleMember("thenify", "withCallback").getACall()
or
this = DataFlow::moduleMember("@google-cloud/promisify", "promisify").getACall()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1912,6 +1912,7 @@ module DataFlow {
deprecated import Configuration
import TypeTracking
import AdditionalFlowSteps
import PromisifyFlow
import internal.FunctionWrapperSteps
import internal.sharedlib.DataFlow
import internal.BarrierGuards
Expand Down
29 changes: 29 additions & 0 deletions javascript/ql/lib/semmle/javascript/dataflow/PromisifyFlow.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Provides data flow steps for promisified user-defined function calls.
* This ensures that when you call a promisified user-defined function,
* arguments flow to the original function's parameters.
*/

private import javascript
private import semmle.javascript.dataflow.AdditionalFlowSteps

/**
* A data flow step from arguments of promisified user-defined function calls to
* the parameters of the original function.
*/
class PromisifiedUserFunctionArgumentFlow extends AdditionalFlowStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(
DataFlow::CallNode promisifiedCall, Promisify::PromisifyCall promisify,
DataFlow::FunctionNode originalFunc, int i
|
// The promisified call flows from a promisify result
promisify.flowsTo(promisifiedCall.getCalleeNode()) and
// The original function was promisified
originalFunc.flowsTo(promisify.getArgument(0)) and
// Argument i of the promisified call flows to parameter i of the original function
pred = promisifiedCall.getArgument(i) and
succ = originalFunc.getParameter(i)
)
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
const express = require('express');
const bodyParser = require('body-parser');
const cp = require('child_process');

const app = express();
app.use(bodyParser.json());

function legacyEval(code) {
cp.exec(code.code); // $ Alert
}

app.post('/eval', async (req, res) => {
const { promisify } = require('util');
const evalAsync = promisify(legacyEval);
const code = req.body; // $ Source
evalAsync(code);
});

app.post('/eval', async (req, res) => {
const directPromisify = require('util.promisify');
const code = req.body; // $ Source

const promisifiedExec3 = directPromisify(cp.exec);
promisifiedExec3(code); // $ Alert
});

app.post('/eval', async (req, res) => {
const promisify2 = require('util.promisify-all');
const promisifiedCp = promisify2(cp);
Comment on lines +28 to +29
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The variable name 'promisify2' is inconsistent with the naming pattern used elsewhere in the file. Consider using 'promisifyAll' to match the library's functionality and improve clarity.

Suggested change
const promisify2 = require('util.promisify-all');
const promisifiedCp = promisify2(cp);
const promisifyAll = require('util.promisify-all');
const promisifiedCp = promisifyAll(cp);

Copilot uses AI. Check for mistakes.
const code = req.body; // $ Source
promisifiedCp.exec(code); // $ Alert
});


app.post('/eval', async (req, res) => {
var garPromisify = require("@gar/promisify");
const code = req.body; // $ Source

const promisifiedExec = garPromisify(cp.exec);
promisifiedExec(code); // $ Alert

const promisifiedCp = garPromisify(cp);
promisifiedCp.exec(code); // $ Alert
});

app.post('/eval', async (req, res) => {
require('util.promisify/shim')();
const util = require('util');
const code = req.body; // $ Source

const promisifiedExec = util.promisify(cp.exec);
promisifiedExec(code); // $ Alert

const execAsync = util.promisify(cp.exec.bind(cp));
execAsync(code); // $ Alert
});


app.post('/eval', async (req, res) => {
const es6Promisify = require("es6-promisify");
let cmd = req.body; // $ Source

// Test basic promisification
const promisifiedExec = es6Promisify(cp.exec);
promisifiedExec(cmd); // $ Alert

// Test with method binding
const execBoundAsync = es6Promisify(cp.exec.bind(cp));
execBoundAsync(cmd); // $ Alert

const promisifiedExecMulti = es6Promisify(cp.exec, {
multiArgs: true
});
promisifiedExecMulti(cmd); // $ Alert

const promisifiedCp = es6Promisify.promisifyAll(cp);
promisifiedCp.exec(cmd); // $ Alert
promisifiedCp.execFile(cmd); // $ Alert
promisifiedCp.spawn(cmd); // $ Alert

const lambda = es6Promisify((code, callback) => {
try {
const result = cp.exec(code); // $ Alert
callback(null, result);
} catch (err) {
callback(err);
}
});
lambda(cmd);
});


app.post('/eval', async (req, res) => {
var thenifyAll = require('thenify-all');
var cpThenifyAll = thenifyAll(require('child_process'), {}, [
'exec',
'execSync',
]);
const code = req.body; // $ Source
cpThenifyAll.exec(code); // $ Alert
cpThenifyAll.execSync(code); // $ Alert
cpThenifyAll.execFile(code); // $ SPURIOUS: Alert - not promisified, as it is not listed in `thenifyAll`, but it should fine to flag it
Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment contains a grammatical error. 'but it should fine to flag it' should be 'but it should be fine to flag it'.

Suggested change
cpThenifyAll.execFile(code); // $ SPURIOUS: Alert - not promisified, as it is not listed in `thenifyAll`, but it should fine to flag it
cpThenifyAll.execFile(code); // $ SPURIOUS: Alert - not promisified, as it is not listed in `thenifyAll`, but it should be fine to flag it

Copilot uses AI. Check for mistakes.


var cpThenifyAll1 = thenifyAll.withCallback(require('child_process'), {}, ['exec']);
cpThenifyAll1.exec(code, function (err, string) {}); // $ Alert

var cpThenifyAll2 = thenifyAll(require('child_process'));
cpThenifyAll2.exec(code); // $ Alert
});

app.post('/eval', async (req, res) => {
const maybe = require('call-me-maybe');
const code = req.body; // $ Source

Copy link

Copilot AI Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is unnecessary trailing whitespace on line 115. This should be removed for consistency with the rest of the codebase.

Suggested change

Copilot uses AI. Check for mistakes.
function createExecPromise(cmd) {
return new Promise((resolve) => {
resolve(cmd);
});
}

const cmdPromise = createExecPromise(code);
maybe(null, cmdPromise).then(cmd => {
cp.exec(cmd); // $ Alert
});
});

app.post('/eval', async (req, res) => {
const utilPromisify = require('util-promisify');
const code = req.body; // $ Source

const promisifiedExec = utilPromisify(cp.exec);
promisifiedExec(code); // $ Alert

const execAsync = utilPromisify(cp.exec.bind(cp));
execAsync(code); // $ Alert
});

app.post('/eval', async (req, res) => {
const {promisify, promisifyAll} = require('@google-cloud/promisify');
const code = req.body; // $ Source

const promisifiedExec = promisify(cp.exec);
promisifiedExec(code); // $ Alert

const execAsync = promisify(cp.exec.bind(cp));
execAsync(code); // $ Alert

const promisifiedCp = promisifyAll(cp);
promisifiedCp.exec(code); // $ Alert
promisifiedCp.execFile(code); // $ Alert
promisifiedCp.spawn(code); // $ Alert
});