diff --git a/tests/tools/test_hardline_blocklist.py b/tests/tools/test_hardline_blocklist.py index 29138c836a4..669c867dc56 100644 --- a/tests/tools/test_hardline_blocklist.py +++ b/tests/tools/test_hardline_blocklist.py @@ -200,6 +200,54 @@ def test_quoted_and_brace_paths_are_hardline_blocked(command): assert desc +# Commands that carry the literal string "rm -rf /" (or a sibling) as DATA in +# another command's quoted argument — a PR title, a commit message, an echo / +# printf argument. The shell never executes that text as an rm command, so the +# hardline floor must NOT fire; otherwise the command cannot run at all (this +# blocked `gh pr create --title "…rm -rf /…"` outright). Regression guard for +# the command-position anchor on the rm rules. +_DATA_ARG_NOT_A_COMMAND = [ + 'gh pr create --title "block rm -rf / spellings"', + 'git commit -m "fixes rm -rf / bypass"', + 'echo "run rm -rf / now"', + 'echo "rm -rf /"', + 'printf "%s" "rm -rf /"', + 'gh issue comment 1 --body "the fix blocks rm -rf //"', +] + + +@pytest.mark.parametrize("command", _DATA_ARG_NOT_A_COMMAND) +def test_root_wipe_string_as_data_arg_is_not_hardline(command): + """"rm -rf /" as a quoted argument to another command is data, not a wipe.""" + is_hl, desc = detect_hardline_command(command) + assert not is_hl, f"false positive: quoted data arg hit hardline floor: {command!r} ({desc})" + + +# Real root wipes at every command position — bare, chained after a separator, +# inside a command substitution ($()/backtick), or after sudo/env wrappers. +# The command-position anchor must keep catching all of these; the substitution +# forms exercise the shell-metacharacter terminator on the bare path branch. +_COMMAND_POSITION_ROOT_WIPES = [ + "rm -rf /", + "ls && rm -rf /", + "ls; rm -rf /", + "echo x | rm -rf /", + "sudo rm -rf /", + "env X=1 rm -rf /", + "$(rm -rf /)", + "`rm -rf /`", + 'echo "$(rm -rf /)"', +] + + +@pytest.mark.parametrize("command", _COMMAND_POSITION_ROOT_WIPES) +def test_root_wipe_at_command_position_is_hardline(command): + """A real `rm -rf /` at any command position stays hardline-blocked.""" + is_hl, desc = detect_hardline_command(command) + assert is_hl, f"real root wipe leaked past the floor: {command!r}" + assert desc + + # ------------------------------------------------------------------------- # Shell line-continuation bypass # ------------------------------------------------------------------------- diff --git a/tools/approval.py b/tools/approval.py index 6bdae8f2dbf..8cb2faf218d 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -332,12 +332,12 @@ _CMDPOS = ( # `rm -rf "/"` slip past the unconditional floor entirely. # # Accept the path either fully wrapped in a matching quote pair OR bare with -# a whitespace/end terminator. The matching-quote requirement is deliberate: -# it catches `rm -rf "/"` (path quoted on its own) while NOT firing on a -# dangerous-looking string that is merely an argument to another command — -# e.g. `git commit -m "rm -rf /"` — where the closing quote follows the path -# but no opening quote precedes it, so neither branch applies. -def _hardline_rm_path(path_alt: str, tail: str = r'(?:\s|$)') -> str: +# a terminator. The matching-quote branch catches `rm -rf "/"` (path quoted +# on its own). The bare branch's terminator accepts whitespace, end-of-string +# OR a shell metacharacter (`) ` ; | &`) so a real root wipe inside a command +# substitution — `$(rm -rf /)`, `` `rm -rf /` `` — whose `/` is terminated by +# `)`/backtick is still caught. +def _hardline_rm_path(path_alt: str, tail: str = r'(?:\s|$|[)`;|&])') -> str: return rf'(?:["\'](?:{path_alt})["\']|(?:{path_alt}){tail})' @@ -350,7 +350,17 @@ _HARDLINE_SYSTEM_DIRS = ( # `rm` plus its flag group, shared by the three rm hardline rules. Kept as a # plain concatenation (not an f-string) so the regex backslashes never live # inside an f-string replacement field — unsupported on the Python 3.11 floor. -_RM_FLAG_PREFIX = r'\brm\s+(-[^\s]*\s+)*' +# +# Anchored to _CMDPOS (start of line, after a command separator ; && || |, +# after a subshell opener $(/backtick, or after sudo/env/exec wrappers) so the +# rule fires only when `rm` is an actual command word — not when the literal +# string "rm -rf /" appears as DATA inside another command's argument, e.g. +# `gh pr create --title "block rm -rf / spellings"` or `git commit -m "…rm -rf +# /…"`. Those tripped the unconditional floor and could not run at all before +# the anchor. A real wipe at any command position (bare, chained, in $()/`…`, +# under sudo) still matches; the quoted-path branch in _hardline_rm_path keeps +# catching `rm -rf "/"`. +_RM_FLAG_PREFIX = _CMDPOS + r'rm\s+(-[^\s]*\s+)*' HARDLINE_PATTERNS = [ # rm recursive targeting the root filesystem or protected roots.