mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(security): anchor rm hardline rules to command position (#56193)
A literal "rm -rf /" carried as DATA inside another command's quoted argument — a PR title, a git commit -m message, an echo/printf arg — tripped the unconditional root-filesystem hardline and could not run at all. `gh pr create --title "block rm -rf / spellings"` was blocked outright, because the bare rm path branch matched the mid-string "rm" (via \brm) with the space after "/" satisfying its (\s|$) terminator. Anchor the shared _RM_FLAG_PREFIX to _CMDPOS so the rm hardline rules fire only when rm is an actual command word (start of line, after a separator ; && || |, after a subshell opener $()/backtick, or after sudo/env/exec wrappers) — not when the string appears as an argument value. Broaden the bare-path terminator to also accept shell metacharacters ) ` ; | & so a real wipe inside a command substitution is still caught. The quoted-path branch is unchanged, so quoted root/HOME paths stay blocked. Adds regression tests for both directions: data-arg false positives must NOT block, real wipes at every command position must block.
This commit is contained in:
parent
6e97f5c3f8
commit
7534b5be2c
2 changed files with 65 additions and 7 deletions
|
|
@ -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
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue