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", () => {
|
it("keeps ordinary shell work available", () => {
|
||||||
expect(isProtectedComputerLifecycleCommand("pwd && ls -la")).toBe(false);
|
expect(isProtectedComputerLifecycleCommand("pwd && ls -la")).toBe(false);
|
||||||
expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false);
|
expect(isProtectedComputerLifecycleCommand("node scripts/check.js")).toBe(false);
|
||||||
|
|||||||
@ -8,6 +8,11 @@
|
|||||||
import { parse as parseShellCommand } from "shell-quote";
|
import { parse as parseShellCommand } from "shell-quote";
|
||||||
|
|
||||||
const SHELL_INTERPRETER_NAMES = /^(?:bash|sh|dash|zsh|ksh|fish)$/;
|
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>> = {
|
const STATIC_SHELL_EXPANSIONS: Readonly<Record<string, string>> = {
|
||||||
HOME: "/home/rakazo",
|
HOME: "/home/rakazo",
|
||||||
LOGNAME: "rakazo",
|
LOGNAME: "rakazo",
|
||||||
@ -42,6 +47,33 @@ function shellCFlagProgram(words: string[], interpreterIndex: number): string |
|
|||||||
return undefined;
|
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 {
|
function preserveShellCommandBoundaries(command: string): string {
|
||||||
let quote: "'" | '"' | undefined;
|
let quote: "'" | '"' | undefined;
|
||||||
let result = "";
|
let result = "";
|
||||||
@ -169,9 +201,14 @@ export function isProtectedComputerLifecycleCommand(command: string): boolean {
|
|||||||
|
|
||||||
for (let index = 0; index < words.length; index += 1) {
|
for (let index = 0; index < words.length; index += 1) {
|
||||||
const name = words[index]?.split("/").at(-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);
|
const program = shellCFlagProgram(words, index);
|
||||||
if (program && isProtectedComputerLifecycleCommand(program)) return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user