Skip to content

Security: dangerous-command denylist (DANGEROUS_PATTERNS / HARDLINE_PATTERNS) is bypassable with trivial shell escapes → silent RCE #36846

@dhyabi2

Description

@dhyabi2

Summary

The dangerous-command safety gate in tools/approval.py classifies commands with a regex denylist (DANGEROUS_PATTERNS, and the newer HARDLINE_PATTERNS) matched against a lightly-normalized command string. Because the string is matched before the shell performs backslash/quote removal, parameter expansion, and command substitution, any shell-level encoding of a blocked keyword defeats the regex while preserving execution behavior. The command then runs via subprocess.run(..., shell=True) with no approval prompt — silent arbitrary command execution under the hermes user in any non-interactive context (cron, RL rollouts, subagents).

This is the core safety boundary the project's own SECURITY.md describes as protecting the operator from LLM actions — so it is squarely in scope under the documented trust model.

Affected code (current main, ef3a650f05d2)

  • tools/approval.py:336DANGEROUS_PATTERNS (regex list, \brm\b, \bchmod\b, etc.)
  • tools/approval.py:218HARDLINE_PATTERNS (unconditional blocklist — also pure regex)
  • tools/approval.py:485_normalize_command_for_detection (strips ANSI/null/NFKC only — no shell tokenization)
  • tools/approval.py:503detect_dangerous_command / :289detect_hardline_command

Verified bypass (run against current main tools/approval.py)

 DANGEROUS  HARDLINE  command
      True     False  rm -rf /home/victim      ← baseline, correctly flagged
     False     False  r\m -rf /home/victim     ← BYPASS
     False     False  r''m -rf /home/victim    ← BYPASS
     False     False  $(echo rm) -rf /home/victim   ← BYPASS
     False     False  ${0/x/r}m -rf /home/victim    ← BYPASS
      True      True  rm -rf /                 ← only the literal form is caught

All four encodings parse to rm under bash (r\m, empty-quote split, command substitution, parameter substitution) but evade both the DANGEROUS and the new HARDLINE lists. The hardline root-delete guard (rm -rf /) is itself bypassable with the same r\m -rf / trick.

Reproduction

from tools.approval import detect_dangerous_command, detect_hardline_command
assert detect_dangerous_command("rm -rf /home/x")[0] is True       # flagged
assert detect_dangerous_command(r"r\m -rf /home/x")[0] is False    # bypass
assert detect_hardline_command(r"r\m -rf /")[0] is False           # bypass hardline too
# → detect_* returns not-dangerous → approval layer returns approved → bash runs rm

Impact

On a non-dangerous classification the approval layer returns approved with no prompt; in non-interactive deployments this is silent RCE under the hermes account. Interactive users simply never see the approval prompt for the dangerous command — defeating the last line of defense. CWE-184 (Incomplete List of Disallowed Inputs), CWE-78 (OS Command Injection). The same weak-regex pattern is reused elsewhere (tui_gateway — see sibling issue).

Suggested remediation

Stop classifying shell strings with regex. Tokenize with shlex.split(), resolve each statement separator (;, |, &&, ||), strip backslash/empty-quote escapes, recurse into $(…)/backticks, and match each resolved argv[0] against a normalized program-name set. Optionally cross-check the post-expansion command from a locked-down bash -c 'printf %s' dry run. Regex on the raw string cannot soundly classify shell commands.


Prior private disclosure & status

This was reported privately via GitHub Security Advisory on 2026-04-23 (advisory GHSA-gmqw-rqrf-c48w, "Dangerous-command denylist bypass and platform-adapter approval fail-opens", rated Critical). The advisory was closed on 2026-05-14 without a fix, without a CVE, and without publication. As shown above, the bypass still reproduces on current main (ef3a650f05d2, 2026-06-01) — the subsequently-added HARDLINE_PATTERNS list does not address it. Opening publicly so the regression has a tracked, visible record. (A related fail-open from the same advisory has its own issue; the Telegram item from that advisory was independently fixed in #24457.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Critical — data loss, security, crash looptool/terminalTerminal execution and process managementtype/securitySecurity vulnerability or hardening

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions