fix(adapters): block kill/pkill via python -c and node -e (#78)

The desktop-protection shell guard only walked nested shell -c programs,
so interpreter inline code could still invoke kill/pkill. Inspect python/
node/perl/ruby/php -c/-e/-r payloads for the same lifecycle kills.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
cynarAI 2026-09-22 22:09:59 +02:00 committed by GitHub
parent 8ad8040d60
commit 1741cdeea6
2 changed files with 72 additions and 3 deletions

View File

@ -32,6 +32,38 @@ describe("computer lifecycle command guard", () => {
}
});
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("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,
);
expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false);
expect(isProtectedComputerLifecycleCommand("python3 scripts/train.py --epochs 2")).toBe(false);
});
it("keeps ordinary shell work available", () => {
expect(isProtectedComputerLifecycleCommand("pwd && ls -la")).toBe(false);
expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false);

View File

@ -8,6 +8,11 @@
import { parse as parseShellCommand } from "shell-quote";
const SHELL_INTERPRETER_NAMES = /^(?:bash|sh|dash|zsh|ksh|fish)$/;
/** Interpreters whose `-c` / `-e` / `-r` payloads can shell out to kill/pkill. */
const SCRIPT_INLINE_INTERPRETER_NAMES = /^(?:python3?|node|nodejs|perl|ruby|php)$/;
/** Lifecycle kill tokens inside an interpreter program string (not shell words). */
const INLINE_PROGRAM_LIFECYCLE_KILL =
/\b(?:pkill|killall|xkill)\b|(?:os|process)\.kill\b|(['"`])kill(?:\s+-\w+|\s+\d+|\1)|\[\s*['"]kill['"]/i;
const STATIC_SHELL_EXPANSIONS: Readonly<Record<string, string>> = {
HOME: "/home/rakazo",
LOGNAME: "rakazo",
@ -42,6 +47,33 @@ function shellCFlagProgram(words: string[], interpreterIndex: number): string |
return undefined;
}
/**
* Extract an inline program from python/node/perl/ruby/php-style argv
* (`-c`/`-e`/`-r`/`-p`, `--eval`, glued forms like `-e'…'`).
*/
function scriptInlineProgram(words: string[], interpreterIndex: number): string | undefined {
for (let index = interpreterIndex + 1; index < words.length; index += 1) {
const word = words[index] ?? "";
if (word.startsWith("--eval=")) return word.slice("--eval=".length);
if (word.startsWith("--command=")) return word.slice("--command=".length);
if (word === "--eval" || word === "--command") return words[index + 1];
// Glued short form: node -e'require…' / python -c'…' / php -r'…'
const glued = /^-(?:[^-]*?)([cerp])(.+)$/.exec(word);
if (glued?.[1] && glued[2] !== undefined && glued[2] !== "") return glued[2];
// python -O -c CODE / node -e CODE / perl -e / ruby -e / php -r / node -p
if (/^-[^-]*[cerp]$/.test(word) || word === "-c" || word === "-e" || word === "-r" || word === "-p") {
return words[index + 1];
}
}
return undefined;
}
function inlineProgramTargetsProtectedLifecycle(program: string): boolean {
// Interpreter source is not shell syntax — do not re-tokenize as a shell command
// (f-strings, template literals, and `$…` would fail-closed incorrectly).
return INLINE_PROGRAM_LIFECYCLE_KILL.test(program);
}
function preserveShellCommandBoundaries(command: string): string {
let quote: "'" | '"' | undefined;
let result = "";
@ -169,9 +201,14 @@ export function isProtectedComputerLifecycleCommand(command: string): boolean {
for (let index = 0; index < words.length; index += 1) {
const name = words[index]?.split("/").at(-1) ?? "";
if (!SHELL_INTERPRETER_NAMES.test(name)) continue;
if (SHELL_INTERPRETER_NAMES.test(name)) {
const program = shellCFlagProgram(words, index);
if (program && isProtectedComputerLifecycleCommand(program)) return true;
continue;
}
if (!SCRIPT_INLINE_INTERPRETER_NAMES.test(name)) continue;
const program = scriptInlineProgram(words, index);
if (program && inlineProgramTargetsProtectedLifecycle(program)) return true;
}
return false;
}