From 2e2a0dfe8b152438a13fc2c1557b54cedc208c32 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:35:48 -0700 Subject: [PATCH] feat(approvals): detect exec-via-flag escapes on read-only commands (port kilocode#11890) Flags on otherwise read-only commands that execute an arbitrary program (sort --compress-program, rg --pre/--hostname-bin, ag --pager, man -P/--pager/--html) were invisible to approval detection: the flag value is opaque argument text, so 'sort --compress-program=sh f' ran without a prompt and a hardline payload smuggled through the flag ('sort --compress-program="rm -rf /" f') bypassed the unconditional floor entirely. Two layers, mirroring Kilo-Org/kilocode#11890: - New DANGEROUS_PATTERNS entries flag the mechanism itself, so the command requires approval even when the payload is a script whose contents we cannot see. - _exec_flag_payloads() surfaces each flag's program value as its own detection variant in _command_detection_variants(), so hardline payloads anchor at command position and hit the floor. E2E: 11 attack shapes detected, 5 hardline payloads reach the floor, 11 legit commands (rg --pretty, grep -P, pip install --pre, man -k pager) unflagged. 554 approval-suite tests green. --- tests/tools/test_exec_via_flag_guard.py | 100 ++++++++++++++++++++++++ tools/approval.py | 69 ++++++++++++++++ 2 files changed, 169 insertions(+) create mode 100644 tests/tools/test_exec_via_flag_guard.py diff --git a/tests/tools/test_exec_via_flag_guard.py b/tests/tools/test_exec_via_flag_guard.py new file mode 100644 index 00000000000..26b0da6aed4 --- /dev/null +++ b/tests/tools/test_exec_via_flag_guard.py @@ -0,0 +1,100 @@ +"""Exec-via-flag escape detection (port of Kilo-Org/kilocode#11890). + +Otherwise read-only commands can execute an arbitrary program through a +flag: ``sort --compress-program``, ``rg --pre`` / ``--hostname-bin``, +``ag --pager``, ``man -P`` / ``--pager`` / ``-H`` / ``--html``. Two layers +are under test: + +1. The mechanism itself is flagged dangerous (approval required) even when + the payload is opaque (``--pre=sh``). +2. A hardline payload smuggled through the flag value reaches the + unconditional floor via ``_exec_flag_payloads`` detection variants. +""" + +import pytest + +from tools.approval import ( + _exec_flag_payloads, + detect_dangerous_command, + detect_hardline_command, +) + + +@pytest.mark.parametrize("command", [ + "sort --compress-program='rm -rf ~' names.txt", + 'sort -S 1b --compress-program "sh" names.txt', + "sort --compress-program=sh names.txt", + "rg --pre sh -e . names.txt", + "rg --pre=sh -e . names.txt", + "rg --hostname-bin=sh pattern", + "ag --pager sh foo", + "ag --pager=sh foo", + "man -P sh ls", + "man -Psh ls", + "man --pager=sh ls", + "man --html=sh ls", +]) +def test_exec_via_flag_is_dangerous(command): + is_dangerous, _key, desc = detect_dangerous_command(command) + assert is_dangerous, f"exec-via-flag escape not detected: {command}" + + +@pytest.mark.parametrize("command", [ + # A hardline payload inside the flag value must reach the unconditional + # floor, not just the softer dangerous prompt. + "sort --compress-program='rm -rf --no-preserve-root /' names.txt", + 'sort --compress-program "rm -rf --no-preserve-root /" names.txt', + "rg --pre 'rm -rf --no-preserve-root /' -e . x", + "man -P 'rm -rf --no-preserve-root /' ls", + # man -H (browser) is invisible to the lowercased dangerous-pattern + # matcher (would collide with `man -h` help), but the payload extractor + # runs on the original-case command, so a dangerous payload still + # reaches the floor. + "man -H 'rm -rf --no-preserve-root /' ls", + "ag --pager 'rm -rf --no-preserve-root /' foo", +]) +def test_hardline_payload_in_exec_flag_hits_floor(command): + is_hardline, desc = detect_hardline_command(command) + assert is_hardline, f"hardline payload escaped the floor: {command}" + assert desc == "recursive delete of root filesystem" + + +@pytest.mark.parametrize("command", [ + "sort names.txt", + "sort -k2 -n data.csv -o out.csv", + "rg 'compress-program' docs/", + "rg -n pattern src/", + "rg --pretty pattern src/", # --pre must not match --pretty + "ag foo src/", + "man ls", + "man -k pager", # -k arg happens to be the word "pager" + "grep -P 'foo(?=bar)' file.txt", # -P is perl-regex on grep, not a pager + "pip install --pre somepackage", # --pre without rg context + "git log --oneline", +]) +def test_legit_commands_not_flagged(command): + is_hardline, _ = detect_hardline_command(command) + is_dangerous, _key, desc = detect_dangerous_command(command) + assert not is_hardline, f"legit command hardlined: {command}" + assert not (is_dangerous and "via" in (desc or "")), ( + f"legit command flagged as exec-via-flag: {command} ({desc})" + ) + + +def test_exec_flag_payload_extraction(): + payloads = list(_exec_flag_payloads( + "sort --compress-program='rm -rf /' a.txt")) + assert payloads == ["rm -rf /"] + + payloads = list(_exec_flag_payloads("man -Psh ls")) + assert payloads == ["sh"] + + payloads = list(_exec_flag_payloads("rg --pre=sh -e . x")) + assert payloads == ["sh"] + + # Long flag with no rg/man/sort context still extracts, but a benign + # payload matches no dangerous pattern (defense-in-depth only). + assert list(_exec_flag_payloads("sort data.txt")) == [] + + # A flag value that is itself another flag is not a payload. + assert list(_exec_flag_payloads("man --pager --html ls")) == [] diff --git a/tools/approval.py b/tools/approval.py index c6866850c2c..8fcec94dac2 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -612,6 +612,28 @@ DANGEROUS_PATTERNS = [ (rf'\btee\b.*["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_WRITE_TARGET_BOUNDARY}', "overwrite project env/config via tee"), (rf'>>?\s*["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_WRITE_TARGET_BOUNDARY}', "overwrite project env/config via redirection"), (r'\bxargs\s+.*\brm\b', "xargs with rm"), + # Exec-via-flag escapes on otherwise read-only commands (port of + # Kilo-Org/kilocode#11890). These flags make an innocuous-looking + # command execute an ARBITRARY PROGRAM: sort spawns its compressor for + # temp runs, rg runs a preprocessor per file, ag/man spawn a pager or + # browser. Without these rules the flag value is opaque argument text, + # so `sort --compress-program='rm -rf ~' f` looked like a plain sort. + # The payload is additionally surfaced to the hardline floor via + # _exec_flag_payloads() in _command_detection_variants below — these + # entries make the *mechanism itself* require approval even when the + # payload points at a script whose contents we cannot see. + (r'\bsort\b[^\n;|&]*--compress-program', "arbitrary program execution via sort --compress-program"), + (r'\brg\b[^\n;|&]*--pre[=\s]', "arbitrary program execution via rg --pre"), + (r'\brg\b[^\n;|&]*--hostname-bin', "arbitrary program execution via rg --hostname-bin"), + (r'\bag\b[^\n;|&]*--pager', "arbitrary program execution via ag --pager"), + # man -P/--pager runs an arbitrary pager program; -H/--html runs an + # arbitrary browser. Detection input is lowercased, so `-p` here also + # covers the rare legit `man -p` (preprocessor list) — an acceptable + # approval-prompt false positive. `(?!-)` keeps `--help`/`--html` long + # flags out of the short-flag branch. + (r'\bman\b[^\n;|&]*\s-(?!-)\w*p', "arbitrary program execution via man pager flag"), + (r'\bman\b[^\n;|&]*\s--pager', "arbitrary program execution via man pager flag"), + (r'\bman\b[^\n;|&]*\s--html', "arbitrary program execution via man --html"), # find -exec rm / -execdir rm — the -execdir variant (same semantics, # runs in the directory of each match) was previously missed. Claude # Code 2.1.113 tightened their equivalent find rule to stop auto- @@ -1349,10 +1371,57 @@ def _iter_shell_command_word_spans(command: str): break +_EXEC_FLAG_PAYLOAD_RE = re.compile( + r"""(?:--compress-program|--pre|--hostname-bin|--pager|--html) + (?:=|\s+) # =value or separate word + (?P'[^']*'|"[^"]*"|\S+)""", + re.VERBOSE, +) + +# man -P/-H short flags (possibly bundled, e.g. ``man -Psh ls``). Scoped to a +# ``man`` command context: ``-P`` is a common flag elsewhere (``grep -P`` is a +# perl-regex switch) and extracting its argument there would false-positive on +# legitimate searches for dangerous-looking strings. +_MAN_EXEC_FLAG_PAYLOAD_RE = re.compile( + r"""\bman\b[^\n;|&]*? + \s-\w*[PH] + (?:=|\s*) # bundled (-Psh), separate word, or = + (?P'[^']*'|"[^"]*"|\S+)""", + re.VERBOSE, +) + + +def _exec_flag_payloads(command: str): + """Yield the program values passed to exec-via-flag options. + + Flags like ``sort --compress-program``, ``rg --pre``, ``ag --pager`` and + ``man -P`` make an otherwise read-only command execute an arbitrary + program. The flag value is normally opaque argument text, so a hardline + payload smuggled through it (``sort --compress-program='rm -rf /' f``) + never reached the unconditional floor. Surfacing each payload as its own + detection variant lets the hardline/dangerous patterns match the wrapped + command directly. Port of Kilo-Org/kilocode#11890. + """ + for regex in (_EXEC_FLAG_PAYLOAD_RE, _MAN_EXEC_FLAG_PAYLOAD_RE): + for match in regex.finditer(command): + payload = _strip_optional_shell_quotes(match.group("payload")) + payload = payload.strip() + if payload and not payload.startswith("-"): + yield payload + + def _command_detection_variants(command: str): normalized = _normalize_command_for_detection(command) seen = {normalized} yield normalized + # Exec-via-flag payloads: expose the program argument of flags that make + # a read-only command execute an arbitrary program, so a hardline command + # hidden inside (e.g.) --compress-program='rm -rf /' anchors at command + # position and hits the unconditional floor. + for payload in _exec_flag_payloads(normalized): + if payload not in seen: + seen.add(payload) + yield payload # Subshell `(cmd)` and brace-group `{ cmd; }` openers put `cmd` at a real # command position, but the flat `_CMDPOS`-anchored patterns can't see it: # their start-position class deliberately omits `(`/`{` because a bare