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:
parent
8ad8040d60
commit
1741cdeea6
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user