AikidoSec-safe-chain/packages/safe-chain/src/utils/safeSpawn.spec.js
2025-10-24 17:36:51 +02:00

214 lines
6.9 KiB
JavaScript

import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert";
describe("safeSpawn", () => {
let safeSpawn;
let spawnCalls = [];
let os;
beforeEach(async () => {
spawnCalls = [];
os = "win32"; // Test Windows behavior by default
// Mock child_process module to capture what command string gets built
mock.module("child_process", {
namedExports: {
spawn: (command, options) => {
spawnCalls.push({ command, options });
return {
on: (event, callback) => {
if (event === "close") {
// Simulate immediate success
setTimeout(() => callback(0), 0);
}
},
};
},
execSync: (cmd) => {
// Simulate 'command -v' returning full path
const match = cmd.match(/command -v (.+)/);
if (match) {
return `/usr/bin/${match[1]}\n`;
}
return "";
},
},
});
mock.module("os", {
namedExports: {
platform: () => os,
},
});
// Import after mocking
const safeSpawnModule = await import("./safeSpawn.js");
safeSpawn = safeSpawnModule.safeSpawn;
});
afterEach(() => {
mock.reset();
});
it("should pass basic command and arguments correctly", async () => {
await safeSpawn("echo", ["hello"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, "echo hello");
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should escape arguments containing spaces", async () => {
await safeSpawn("echo", ["hello world"]);
assert.strictEqual(spawnCalls.length, 1);
// Argument should be escaped to prevent shell interpretation
assert.strictEqual(spawnCalls[0].command, 'echo "hello world"');
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should prevent shell injection attacks", async () => {
await safeSpawn("ls", ["; rm test123.txt"]);
assert.strictEqual(spawnCalls.length, 1);
// Malicious command should be escaped to prevent execution
assert.strictEqual(spawnCalls[0].command, 'ls "; rm test123.txt"');
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should escape single quotes in arguments", async () => {
await safeSpawn("echo", ["don't break"]);
assert.strictEqual(spawnCalls.length, 1);
// Single quote should be properly escaped with double quotes
assert.strictEqual(spawnCalls[0].command, 'echo "don\'t break"');
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should handle double quotes with simpler escaping", async () => {
await safeSpawn("echo", ['say "hello"']);
assert.strictEqual(spawnCalls.length, 1);
// If we switch to double quotes, this should be: "say \"hello\""
assert.strictEqual(spawnCalls[0].command, 'echo "say \\"hello\\""');
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should not escape arguments with only safe characters", async () => {
await safeSpawn("npm", ["install", "axios", "--save"]);
assert.strictEqual(spawnCalls.length, 1);
// Safe arguments (alphanumeric, dash, underscore, dot, slash) shouldn't be quoted
assert.strictEqual(spawnCalls[0].command, "npm install axios --save");
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it(`should escape ampersand character`, async () => {
await safeSpawn("npx", ["cypress", "run", "--env", "password=foo&bar"]);
assert.strictEqual(spawnCalls.length, 1);
// & should be escaped by wrapping the arg in quotes
assert.strictEqual(
spawnCalls[0].command,
'npx cypress run --env "password=foo&bar"'
);
assert.strictEqual(spawnCalls[0].options.shell, true);
});
it("should escape dollar signs to prevent variable expansion", async () => {
await safeSpawn("echo", ["$HOME/test"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "\\$HOME/test"');
});
it("should escape backticks to prevent command substitution", async () => {
await safeSpawn("echo", ["file`whoami`.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "file\\`whoami\\`.txt"');
});
it("should escape backslashes properly", async () => {
await safeSpawn("echo", ["path\\with\\backslash"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'echo "path\\\\with\\\\backslash"'
);
});
it("should handle multiple special characters in one argument", async () => {
await safeSpawn("cmd", ['test "quoted" $var `cmd` & more']);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'cmd "test \\"quoted\\" \\$var \\`cmd\\` & more"'
);
});
it("should handle pipe character", async () => {
await safeSpawn("echo", ["foo|bar"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "foo|bar"');
});
it("should handle parentheses", async () => {
await safeSpawn("echo", ["(test)"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "(test)"');
});
it("should handle angle brackets for redirection", async () => {
await safeSpawn("echo", ["foo>output.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "foo>output.txt"');
});
it("should handle wildcard characters", async () => {
await safeSpawn("echo", ["*.txt"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, 'echo "*.txt"');
});
it("should handle multiple arguments with mixed escaping needs", async () => {
await safeSpawn("cmd", ["safe", "needs space", "$dangerous", "also-safe"]);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(
spawnCalls[0].command,
'cmd safe "needs space" "\\$dangerous" also-safe'
);
});
it("should reject command names with special characters", async () => {
await assert.rejects(async () => await safeSpawn("npm; echo hacked", []), {
message: "Invalid command name: npm; echo hacked",
});
});
it("should reject command names with spaces", async () => {
await assert.rejects(async () => await safeSpawn("npm install", []), {
message: "Invalid command name: npm install",
});
});
it("should reject command names with slashes", async () => {
await assert.rejects(async () => await safeSpawn("../../malicious", []), {
message: "Invalid command name: ../../malicious",
});
});
it("should accept valid command names with letters, numbers, underscores and hyphens", async () => {
await safeSpawn("valid_command-123", []);
assert.strictEqual(spawnCalls.length, 1);
assert.strictEqual(spawnCalls[0].command, "valid_command-123");
});
});