python -m, stdin programs, and deno/bun eval or kill-named scripts could still tear down a managed graphical desktop after the shell guard tokenized the command. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
307 lines
13 KiB
TypeScript
307 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { isProtectedComputerLifecycleCommand } from "./executor.js";
|
|
import {
|
|
isProtectedComputerLifecycleCommandReadingScripts,
|
|
protectedScriptWorkspacePath,
|
|
} from "./executor-shell-guard.js";
|
|
|
|
describe("computer lifecycle command guard", () => {
|
|
it("rejects commands that can destroy a graphical bot's desktop", () => {
|
|
for (const command of [
|
|
"pkill chromium",
|
|
"killall chrome",
|
|
"kill -9 1234",
|
|
"k\\ill -9 1234",
|
|
"xkill",
|
|
"systemctl restart chromium",
|
|
"systemctl --user restart chromium",
|
|
"service chromium restart",
|
|
"rm -rf ~/.browser-profiles/chromium",
|
|
'rm -rf "$HOME/.browser-profiles/chromium"',
|
|
"rm -f /tmp/.X1-lock",
|
|
"bash -c 'pkill chromium'",
|
|
'bash -lc "killall chrome"',
|
|
"bash -o posix -c 'pkill chromium'",
|
|
"bash --rcfile /tmp/bashrc -c 'pkill chromium'",
|
|
"sh -c 'systemctl restart chromium'",
|
|
`bash -c 'eval "pkill chromium"'`,
|
|
'bash -lc "source /tmp/kill-chrome.sh"',
|
|
"printf 'pkill chromium\\n' > /tmp/x; . /tmp/x",
|
|
"bash -c `pkill chromium`",
|
|
'bash <<< "pkill chromium"',
|
|
"$KILLER chromium",
|
|
'rm -rf "$TARGET/.browser-profiles/chromium"',
|
|
]) {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects kill/pkill invoked through interpreter inline programs", () => {
|
|
for (const command of [
|
|
`python -c 'import os; os.system("pkill chromium")'`,
|
|
`python3 -c 'import subprocess; subprocess.call(["pkill", "chromium"])'`,
|
|
`python -c 'import os; os.kill(1234, 9)'`,
|
|
`python3 -Oc 'import os; os.system("killall chrome")'`,
|
|
`node -e 'require("child_process").execSync("pkill chromium")'`,
|
|
`node --eval 'process.kill(1234, "SIGKILL")'`,
|
|
`node --eval=process.kill(1,"SIGKILL")`,
|
|
`nodejs -e 'require("child_process").spawnSync("xkill")'`,
|
|
`perl -e 'system("pkill chromium")'`,
|
|
`ruby -e 'system("pkill chromium")'`,
|
|
`php -r 'system("pkill chromium");'`,
|
|
`bash -c 'python -c "import os; os.system(\\"pkill chromium\\")"'`,
|
|
]) {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects kill-family teardown in interpreter script files", () => {
|
|
const files = new Map<string, string>([
|
|
["/tmp/kill.py", "import os\nos.system('pkill chromium')\n"],
|
|
["evil.js", "process.kill(1, 'SIGKILL')\n"],
|
|
["./scripts/stop.py", "import subprocess\nsubprocess.call(['killall', 'chrome'])\n"],
|
|
]);
|
|
const readScript = (scriptPath: string) => files.get(scriptPath);
|
|
|
|
for (const command of [
|
|
"python /tmp/kill.py",
|
|
"node evil.js",
|
|
"python3 ./scripts/stop.py",
|
|
"python3.12 ./scripts/stop.py",
|
|
"bash -c 'python evil.js'",
|
|
"bash -lc 'node evil.js'",
|
|
"node -r evil.js -e 'console.log(1)'",
|
|
]) {
|
|
expect(isProtectedComputerLifecycleCommand(command, readScript)).toBe(true);
|
|
}
|
|
// Filename heuristic when the executor cannot read the script (container /tmp).
|
|
expect(isProtectedComputerLifecycleCommand("python /tmp/kill.py")).toBe(true);
|
|
expect(isProtectedComputerLifecycleCommand("perl /tmp/pkill.pl")).toBe(true);
|
|
expect(isProtectedComputerLifecycleCommand("node /tmp/killall.js")).toBe(true);
|
|
expect(isProtectedComputerLifecycleCommand("ruby xkill.rb")).toBe(true);
|
|
expect(isProtectedComputerLifecycleCommand("/usr/bin/python3 /tmp/kill.py")).toBe(true);
|
|
});
|
|
|
|
it("prefers script contents over the filename heuristic", async () => {
|
|
await expect(
|
|
isProtectedComputerLifecycleCommandReadingScripts(
|
|
"python kill.py",
|
|
async () => "print('hello')\n",
|
|
),
|
|
).resolves.toBe(false);
|
|
await expect(
|
|
isProtectedComputerLifecycleCommandReadingScripts("python kill.py", async () => undefined),
|
|
).resolves.toBe(true);
|
|
await expect(
|
|
isProtectedComputerLifecycleCommandReadingScripts(
|
|
"bash -c 'node evil.js'",
|
|
async () => "process.kill(1, 'SIGKILL')\n",
|
|
),
|
|
).resolves.toBe(true);
|
|
});
|
|
|
|
it("keeps ordinary interpreter inline programs available", () => {
|
|
expect(isProtectedComputerLifecycleCommand(`python -c 'print("hello")'`)).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand(`python3 -c 'value = "$HOME"; print(value)'`)).toBe(
|
|
false,
|
|
);
|
|
expect(isProtectedComputerLifecycleCommand(`node -e 'console.log(1 + 1)'`)).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand(`node -e 'const x = "kill switch"; console.log(x)'`)).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("keeps ordinary interpreter scripts available", () => {
|
|
const files = new Map<string, string>([
|
|
["scripts/check.js", "console.log('ok')\n"],
|
|
["scripts/train.py", "print('epochs')\n"],
|
|
["kill.py", "print('hello')\n"],
|
|
["notes/harmless.py", "value = 'kill switch'\nprint(value)\n"],
|
|
["evil.js", "console.log('not a lifecycle command')\n"],
|
|
]);
|
|
const readScript = (scriptPath: string) => files.get(scriptPath);
|
|
|
|
expect(isProtectedComputerLifecycleCommand("node scripts/check.js", readScript)).toBe(false);
|
|
expect(
|
|
isProtectedComputerLifecycleCommand("python3 scripts/train.py --epochs 2", readScript),
|
|
).toBe(false);
|
|
expect(
|
|
isProtectedComputerLifecycleCommand("python -W ignore scripts/train.py", readScript),
|
|
).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("python kill.py", readScript)).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("python notes/harmless.py", readScript)).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("node evil.js", readScript)).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("echo python /tmp/kill.py")).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("node /tmp/plot.js")).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("python -c 'print(\"hello\")'")).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
`python -m timeit 'os.kill(1, 9)'`,
|
|
`python3 -m timeit 'import os; os.system("pkill chromium")'`,
|
|
`python -m runpy -c 'import os; os.kill(1, 9)'`,
|
|
`python3 -m trace -c 'os.system("pkill chromium")'`,
|
|
`python -m timeit 'subprocess.call(["pkill", "chromium"])'`,
|
|
`python -m timeit 'subprocess.call(["kill", "-9", "1"])'`,
|
|
"python -m scripts.kill",
|
|
"python -m kill.main",
|
|
"python3 -m pkg.pkill",
|
|
"/usr/bin/python3 -m os.kill",
|
|
`python -W ignore -m timeit 'os.kill(1, 9)'`,
|
|
`python3.12 -O -m timeit 'os.kill(1, 9)'`,
|
|
"command python3 -m os.kill",
|
|
"FOO=1 python -m os.kill",
|
|
`echo 'import os; os.kill(1, 9)' | python -`,
|
|
`echo 'pkill chromium' | python3 -`,
|
|
`printf '%s\\n' 'import os; os.system("pkill chromium")' | python3 -`,
|
|
`echo 'process.kill(1)' | node -`,
|
|
`echo 'process.kill(1)' | node --require preload.js -`,
|
|
"python - <<'PY'\np = \"pkill chromium\"\nPY",
|
|
`echo 'os.kill(1)' | python > /tmp/out -`,
|
|
`echo 'system("pkill chromium")' | perl -`,
|
|
`echo 'system("pkill chromium")' | perl -I lib -`,
|
|
`echo 'system("pkill chromium")' | ruby -`,
|
|
`deno eval 'Deno.kill(1, "SIGKILL")'`,
|
|
`deno eval 'process.kill(1, "SIGKILL")'`,
|
|
`deno eval --ext=ts 'Deno.kill(1, "SIGKILL")'`,
|
|
`echo 'Deno.kill(1)' | deno run -`,
|
|
`bun -e 'process.kill(1, "SIGKILL")'`,
|
|
`bun --eval 'require("child_process").execSync("pkill chromium")'`,
|
|
`bun --eval='process.kill(1)'`,
|
|
`bun -e'process.kill(1)'`,
|
|
"deno run kill.ts",
|
|
"deno run --allow-net kill.ts",
|
|
"deno run --config deno.json kill.ts",
|
|
"deno --quiet run kill.ts",
|
|
"deno run -- kill.ts",
|
|
"deno test kill.ts",
|
|
"bun /tmp/pkill.ts",
|
|
"bun run ./kill.ts",
|
|
"bun src/xkill.js",
|
|
`eval "pkill chromium"`,
|
|
`bash -c 'python -m timeit "os.kill(1, 9)"'`,
|
|
`bash -c 'echo "os.kill(1)" | python -'`,
|
|
`bash -c 'deno run kill.ts'`,
|
|
])("rejects kill-family python -m, stdin, and deno/bun bypasses: %s", (command) => {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
"python -m pip",
|
|
"python3 -m pip install",
|
|
"python -O -m http.server",
|
|
"python3 -q -m json.tool",
|
|
"python -m os",
|
|
"python -m os.path",
|
|
"python -Ompip install",
|
|
"python -W ignore -m pip",
|
|
"python3.12 -m pip",
|
|
"python -m pytest kill.py",
|
|
`python -m timeit 'print("kill switch")'`,
|
|
`python -m timeit 'print("process.killer")'`,
|
|
"python -m myapp.kill_switch",
|
|
"python -",
|
|
`echo 'print("hello")' | python -`,
|
|
`echo 'print("kill switch")' | python3 -`,
|
|
"python - <<'PY'\nprint hello\nPY",
|
|
`echo 'os.kill(1)' | python -m pip`,
|
|
`echo 'os.kill(1)' | python script.py`,
|
|
`python -m pip && echo 'os.kill(1)'`,
|
|
"python3 scripts/train.py",
|
|
"node scripts/check.js",
|
|
`echo 'console.log(1)' | node -`,
|
|
`node -e 'console.log("kill switch")'`,
|
|
`python -c 'print("kill switch")'`,
|
|
`deno eval 'console.log(1)'`,
|
|
`deno eval 'eval(1)'`,
|
|
`deno eval 'console.log("kill switch")'`,
|
|
`deno eval --ext=ts 'console.log("kill switch")'`,
|
|
"deno run src/main.ts",
|
|
"deno run --allow-net=localhost src/main.ts",
|
|
"deno run src/main.ts -- kill.ts",
|
|
"deno fmt kill.ts",
|
|
"deno fmt src/kill.ts",
|
|
"bun src/index.ts",
|
|
"bun run dev",
|
|
"bun run src/index.ts -- kill.ts",
|
|
`bun -e 'console.log(1)'`,
|
|
`bun -e 'console.log("kill switch")'`,
|
|
"bun -p '1+1'",
|
|
"bun install",
|
|
"echo python -m os.kill",
|
|
"echo deno run kill.ts",
|
|
`echo 'puts "hello"' | ruby -`,
|
|
"ruby scripts/check.rb",
|
|
`perl -e 'print "kill switch"'`,
|
|
"command python3 -m http.server",
|
|
])("keeps ordinary python -m, stdin, and deno/bun work: %s", (command) => {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(false);
|
|
});
|
|
|
|
it("maps interpreter scripts onto the workspace file API", () => {
|
|
expect(protectedScriptWorkspacePath("./scripts/stop.py", undefined)).toBe("scripts/stop.py");
|
|
expect(protectedScriptWorkspacePath("evil.js", "notes")).toBe("notes/evil.js");
|
|
expect(protectedScriptWorkspacePath("stop.py", "bots/bot-1")).toBe("bots/bot-1/stop.py");
|
|
expect(protectedScriptWorkspacePath("evil.js", "/home/rakazo/notes")).toBe("notes/evil.js");
|
|
expect(protectedScriptWorkspacePath("/home/rakazo/scripts/stop.py", undefined)).toBe(
|
|
"scripts/stop.py",
|
|
);
|
|
expect(protectedScriptWorkspacePath("/home/user/rakazo-home/evil.js", undefined)).toBe(
|
|
"evil.js",
|
|
);
|
|
expect(protectedScriptWorkspacePath("evil.js", "/tmp")).toBe("tmp/evil.js");
|
|
expect(protectedScriptWorkspacePath("/tmp/kill.py", undefined)).toBeUndefined();
|
|
expect(protectedScriptWorkspacePath("../secret.py", "notes")).toBeUndefined();
|
|
expect(protectedScriptWorkspacePath("evil.js", "/home/user")).toBe("evil.js");
|
|
});
|
|
|
|
it("keeps ordinary shell work available", () => {
|
|
expect(isProtectedComputerLifecycleCommand("pwd && ls -la")).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("systemctl status chromium")).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand('rm -f "$WORKSPACE/tmp.txt"')).toBe(false);
|
|
expect(isProtectedComputerLifecycleCommand("printf '%s\\n' *.txt && pwd")).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
"find . -maxdepth 2 -type d -name .git -print",
|
|
"git -C . status --short",
|
|
"git add .",
|
|
"git add then .",
|
|
"ls . && git -C . worktree list --porcelain",
|
|
"set -eu\npwd\nfind . -maxdepth 2 -type d -name .git -print",
|
|
"git worktree add ../review-worktree origin/main",
|
|
"find /tmp/. -maxdepth 1 -type d",
|
|
"git diff -- .",
|
|
"printf '%s' 'pk\\\nill chromium'",
|
|
"printf '%s' 'pk\\\nill' chromium",
|
|
"printf '%s' 'line one\nline two'",
|
|
])("allows repository paths without treating dot arguments as sourcing: %s", (command) => {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
". /tmp/script.sh",
|
|
"pwd; . /tmp/script.sh",
|
|
"pwd\n. /tmp/script.sh",
|
|
"find . -maxdepth 1 && . /tmp/script.sh",
|
|
"command . /tmp/script.sh",
|
|
"builtin . /tmp/script.sh",
|
|
"command -p . /tmp/script.sh",
|
|
"true && > /tmp/output . /tmp/script.sh",
|
|
"2> /tmp/output . /tmp/script.sh",
|
|
"if true; then . /tmp/script.sh; fi",
|
|
"bash -c 'pwd\n. /tmp/script.sh'",
|
|
"pk\\\nill chromium",
|
|
"! . /tmp/script.sh",
|
|
"if false; then :; elif . /tmp/script.sh; then :; fi",
|
|
"{ . /tmp/script.sh; }",
|
|
"coproc . /tmp/script.sh",
|
|
"coproc worker . /tmp/script.sh",
|
|
"function f { . /tmp/script.sh; }",
|
|
"function f { . /tmp/script.sh; }; f",
|
|
])("continues blocking executable sourcing and lifecycle operations: %s", (command) => {
|
|
expect(isProtectedComputerLifecycleCommand(command)).toBe(true);
|
|
});
|
|
});
|