diff --git a/.changeset/quick-pandas-decode.md b/.changeset/quick-pandas-decode.md new file mode 100644 index 00000000..52c774e8 --- /dev/null +++ b/.changeset/quick-pandas-decode.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": patch +--- + +decode `Buffer.from(payload, "base64")` in require specifiers, so `require(Buffer.from("aHR0cA==", "base64").toString())` records `http` like the `hex` and `atob` forms already do diff --git a/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts b/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts index 47b2d2d6..74379716 100644 --- a/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts +++ b/workspaces/js-x-ray/src/probes/isRequire/RequireCallExpressionWalker.ts @@ -5,7 +5,7 @@ import path from "node:path"; import type { ESTree } from "meriyah"; // Import Internal Dependencies -import { Hex } from "../../utils/index.ts"; +import { Hex, isStringBase64 } from "../../utils/index.ts"; import { arrayExpressionToString, getCallExpressionArguments, @@ -110,10 +110,23 @@ export class RequireCallExpressionWalker { #handleBufferFrom( node: ESTree.CallExpression ) { - const [element] = node.arguments; + const [element, encoding] = node.arguments; if (element.type === "ArrayExpression") { const depName = [...arrayExpressionToString(element)].join("").trim(); this.dependencies.add(depName); + + return; + } + + if ( + isStringLiteral(element) && + isStringLiteral(encoding) && + encoding.value === "base64" && + isStringBase64(element.value, { allowEmpty: false }) + ) { + this.dependencies.add( + Buffer.from(element.value, "base64").toString() + ); } } diff --git a/workspaces/js-x-ray/test/probes/isRequire.spec.ts b/workspaces/js-x-ray/test/probes/isRequire.spec.ts index 2ce4e744..3133fa7d 100644 --- a/workspaces/js-x-ray/test/probes/isRequire.spec.ts +++ b/workspaces/js-x-ray/test/probes/isRequire.spec.ts @@ -341,6 +341,36 @@ describe("isRequire probe", () => { assert.ok(dependencies.has("http")); }); + it("(require CallExpression): should detect MemberExpression Buffer.from (with base64 encoding)", () => { + const str = ` + require(Buffer.from("aHR0cA==", "base64").toString()); + `; + const ast = parseScript(str); + const sastAnalysis = getSastAnalysis(isRequire) + .execute(ast.body); + + assert.strictEqual(sastAnalysis.warnings().length, 1); + const warning = sastAnalysis.getWarning("unsafe-import"); + assert.strictEqual(warning!.kind, "unsafe-import"); + + const dependencies = sastAnalysis.dependencies(); + assert.strictEqual(dependencies.size, 1); + assert.ok(dependencies.has("http")); + }); + + it("(require CallExpression): should not resolve Buffer.from when the payload is not valid base64", () => { + const str = ` + require(Buffer.from("not base64 at all!", "base64").toString()); + `; + const ast = parseScript(str); + const sastAnalysis = getSastAnalysis(isRequire) + .execute(ast.body); + + const warning = sastAnalysis.getWarning("unsafe-import"); + assert.strictEqual(warning!.kind, "unsafe-import"); + assert.strictEqual(sastAnalysis.dependencies().size, 0); + }); + it("(require CallExpression): should detect MemberExpression Buffer.from (with ArrayExpression argument)", () => { const str = ` require(Buffer.from([104, 101, 108, 108, 111]).toString());