fix(security): strip shell escapes in command denylist; fail-closed on missing approval module (#36846 #36847)#40370
Conversation
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved (with a couple of non-blocking enhancements noted inline)
The implementation matches the stated intent:
- tools/approval.py: _normalize_command_for_detection() now strips backslash-escapes and empty-string literals before dangerous-pattern matching, closing the bypass described in the PR. The two re.sub calls are scoped and safe.
- tui_gateway/server.py: shell.exec now calls detect_hardline_command before detect_dangerous_command, and the except ImportError: pass has been replaced with a fail-closed 5001 error response, closing the second issue described in the PR.
Reviewed by Hermes Agent
| if is_hardline: | ||
| return _err( | ||
| rid, 4005, f"blocked (hardline): {hardline_desc}. Use the agent for dangerous commands." | ||
| ) | ||
| is_dangerous, _, desc = detect_dangerous_command(cmd) |
There was a problem hiding this comment.
Non-blocking: consider logging a metric/warning when the fail-closed ImportError path triggers. A silent 5001 is difficult to distinguish from other 5xx failures in production observability. Worth a follow-up issue.
| @@ -537,6 +537,10 @@ def _normalize_command_for_detection(command: str) -> str: | |||
| command = command.replace('\x00', '') | |||
| # Normalize Unicode (fullwidth Latin, halfwidth Katakana, etc.) | |||
| command = unicodedata.normalize('NFKC', command) | |||
| # Strip shell backslash-escapes: r\m → rm. Prevents \-injection bypass. | |||
| command = re.sub(r'\\([^\n])', r'\1', command) | |||
There was a problem hiding this comment.
Non-blocking: add a short comment explaining why these two regexes exist (reference #36846) so a future reviewer who sees them in isolation does not mistake them for dead/obfuscated code.
Review: Mixed concerns — security fix bundled with unrelated compression fixThe security changes in
However, this PR also includes The compression fix has a bug (missing |
…d on missing approval module DANGEROUS_PATTERNS and HARDLINE_PATTERNS are matched on the raw command string, so backslash-escape (r\m) and empty-quote split (r''m) bypass both lists. _normalize_command_for_detection now strips these before pattern matching. tui_gateway shell.exec had a bare 'except ImportError: pass' that silently disabled the entire safety gate if tools.approval wasn't importable. Changed to fail-closed (return 5001 error). Added detect_hardline_command check. Fixes NousResearch#36846, NousResearch#36847.
22fcd15 to
8cade08
Compare
|
Thanks @liuhao1024 — you're right. The context_compressor.py and test changes (the #36718 double-compression fix) were accidentally included because the branch was based on local HEAD rather than upstream/main directly. Cleaned up: reset branch to upstream/main and cherry-picked only the security commit (tools/approval.py + tui_gateway/server.py). The PR now contains exactly the two files it should. Force-pushed. |
Summary
Two related security fixes in the command-safety layer.
#36846 — Denylist bypass via shell escape sequences (
tools/approval.py)DANGEROUS_PATTERNSandHARDLINE_PATTERNSare matched against the output of_normalize_command_for_detection, which previously only stripped ANSI sequences, null bytes, and Unicode full-width variants. Two shell-level obfuscation techniques bypassed it:r\m— the shell removes the backslash and executesrm, but the pattern\brm\bnever matched.r''morr""m— the shell concatenates the tokens intorm, but again the pattern didn't match.Fix: after NFKC normalization, two
re.subcalls now strip these constructs before pattern matching:rewas already imported at the top oftools/approval.py.Not fixed by this PR (out of scope): command-substitution bypasses such as
$(echo rm)require a shell interpreter to evaluate and cannot be handled by static regex matching.#36847 — Fail-open
except ImportError: passintui_gateway/server.pyshell.execwrapped the entire safety check in atry/except ImportError: passblock, meaning that iftools.approvalwas not importable for any reason (missing dep, import-time error, broken install), the safety gate was silently skipped and the command executed anyway.Additionally, only
detect_dangerous_commandwas called — the stricterdetect_hardline_commandcheck was absent.Fix:
except ImportError: pass→except ImportError: return _err(rid, 5001, "shell.exec unavailable: approval safety module not importable")(fail-closed).detect_hardline_commandcheck that runs beforedetect_dangerous_command.Files changed
tools/approval.py_normalize_command_for_detectiontui_gateway/server.pyImportError; adddetect_hardline_commandcheckTest plan
r\m -rf /is blocked byDANGEROUS_PATTERNSafter normalizationr''m -rf /is blocked after normalizationr""m -rf /is blocked after normalizationImportErrorfromtools.approvalinshell.execand confirm a 5001 error is returned instead of the command executingdd if=/dev/zero) is blocked viadetect_hardline_command