fix(security): convert shell=True to shlex.split in cli.py and mcp_catalog.py - #35545
fix(security): convert shell=True to shlex.split in cli.py and mcp_catalog.py#35545someaka wants to merge 1 commit into
Conversation
|
I found one issue worth fixing before merge.
The PR converts
The existing code comment at line 8771 explicitly documents the design decision: # shell=True is intentional: quick_commands are user-defined
# shell snippets from config.yaml — not agent/LLM controlled.
The PR body acknowledges "Shell operators like Also, the stale comment "shell=True is intentional" should be removed or updated if the change is intentional.
Unlike Suggested fix:
|
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved ✅
Review Findings
This PR converts shell=True to shlex.split() + shell=False in two config-driven code paths (quick_commands and MCP catalog bootstrap). This is a well-motivated security hardening change.
✅ Looks Good
- Correctness: The conversion from
shell=Truetoshlex.split()is mechanically correct. Both callers pass single-command strings whereshlex.split()produces the correct argv. - Edge cases: The
cli.pypath already wraps intry/except, and themcp_catalog.pypath already has error handling. No new error surfaces introduced. - Documentation: The docstring update in
mcp_catalog.pyexplicitly notes that&&is no longer supported and directs to use separate command entries — good forward guidance. - Scope: Tightly scoped to files that need it. Notes in the PR body about
docker.pyandtui_gateway/server.pyshow awareness of the broader landscape. - Minimal diff: 8 additions, 5 deletions — small, focused, easy to audit.
No Issues Found
Reviewed by Hermes Agent
6ee491a to
8119334
Compare
|
Thank you for the thorough review @liuhao1024. Both points are valid and addressed: cli.py — reverted, no change You're absolutely right. mcp_catalog.py — ValueError handling added Fixed in 811933419. The changes:
The rationale for keeping Other I audited the remaining
PR updated. Single commit now — only the mcp_catalog.py fix. |
Address review feedback from @liuhao1024 on PR NousResearch#35545: - Wrap shlex.split() in try/except ValueError to handle unclosed quotes or malformed shell syntax gracefully, raising CatalogError with a descriptive message instead of an unhandled exception. - Update docstring to document the shell=False behavior and the limitation on shell operators (&&, ||, |). - Catalog entries should use separate command entries for chained operations instead of shell operators. Note: cli.py quick_commands intentionally keep shell=True — those are user-defined shell snippets from config.yaml, not untrusted input. The original PR incorrectly changed this.
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved ✅
Changes
hermes_cli/mcp_catalog.py: Convertedsubprocess.run(cmd, shell=True)toshlex.split(cmd)+shell=Falsein_run_bootstrap()
Review
🔒 Security ✅
- Correctly removes the
shell=Truepattern that could allow injection from untrusted YAML catalog entries - Uses
shlex.split()for proper tokenization - Malformed commands are caught with a
ValueErrorexception and raised asCatalogError
✅ Correctness
- The try/except around
shlex.split()properly handles edge cases like unmatched quotes - Return code checking is preserved
- Docstring updated to reflect new semantics (no shell operators)
✅ Code Quality
- Small, focused diff — single file, single concern
- Import is scoped inside the function (consistent with existing code style)
- Error messages are descriptive
Summary
Clean security fix. Proper pattern: shlex.split() → list argv → shell=False → error handling. No issues found.
Reviewed by Hermes Agent (cron job)
8119334 to
f7de106
Compare
f7de106 to
56a07ec
Compare
Address review feedback from @liuhao1024 on PR NousResearch#35545: - Wrap shlex.split() in try/except ValueError to handle unclosed quotes or malformed shell syntax gracefully, raising CatalogError with a descriptive message instead of an unhandled exception. - Update docstring to document the shell=False behavior and the limitation on shell operators (&&, ||, |). - Catalog entries should use separate command entries for chained operations instead of shell operators. Note: cli.py quick_commands intentionally keep shell=True — those are user-defined shell snippets from config.yaml, not untrusted input. The original PR incorrectly changed this.
56a07ec to
74029e0
Compare
74029e0 to
5cf03b9
Compare
5cf03b9 to
6c9f254
Compare
Address review feedback from @liuhao1024 on PR NousResearch#35545: - Wrap shlex.split() in try/except ValueError to handle unclosed quotes or malformed shell syntax gracefully, raising CatalogError with a descriptive message instead of an unhandled exception. - Update docstring to document the shell=False behavior and the limitation on shell operators (&&, ||, |). - Catalog entries should use separate command entries for chained operations instead of shell operators. Note: cli.py quick_commands intentionally keep shell=True — those are user-defined shell snippets from config.yaml, not untrusted input. The original PR incorrectly changed this.
6c9f254 to
4f99775
Compare
Address review feedback from @liuhao1024 on PR NousResearch#35545: - Wrap shlex.split() in try/except ValueError to handle unclosed quotes or malformed shell syntax gracefully, raising CatalogError with a descriptive message instead of an unhandled exception. - Update docstring to document the shell=False behavior and the limitation on shell operators (&&, ||, |). - Catalog entries should use separate command entries for chained operations instead of shell operators. Note: cli.py quick_commands intentionally keep shell=True — those are user-defined shell snippets from config.yaml, not untrusted input. The original PR incorrectly changed this.
Address review feedback from @liuhao1024 on PR NousResearch#35545: - Wrap shlex.split() in try/except ValueError to handle unclosed quotes or malformed shell syntax gracefully, raising CatalogError with a descriptive message instead of an unhandled exception. - Update docstring to document the shell=False behavior and the limitation on shell operators (&&, ||, |). - Catalog entries should use separate command entries for chained operations instead of shell operators. Note: cli.py quick_commands intentionally keep shell=True — those are user-defined shell snippets from config.yaml, not untrusted input. The original PR incorrectly changed this.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for narrowing the change after the quick-command review feedback. The target is still present on current main (hermes_cli/mcp_catalog.py:367), but this needs a few decisions and tests before it is safe to salvage.
Problems
- Current main explicitly documents bootstrap shell semantics, including
&&support (hermes_cli/mcp_catalog.py:360-363). The PR removes that contract, while the catalog docs describe manifests as Nous-reviewed and their bootstrap commands as deliberate install actions (website/docs/user-guide/features/mcp.md:117-125). Please make the trust-boundary and compatibility decision explicit. - The new direct argv call can regress Windows npm-style bootstrap commands:
hermes_cli/_subprocess_compat.py:6-9documents that bare npm list argv fails for.cmdshims, but the changed call does not resolve it. - No
_run_bootstrapregression tests are added;tests/hermes_cli/test_mcp_catalog.py:146-167only tests manifest parsing.
Suggested changes
- Add tests for valid argv execution, malformed quoting →
CatalogError, rejected shell operators, and Windows-safe command launch. - Route executable invocation through the established Windows-safe mechanism if argv-only execution is adopted.
Automated hermes-sweeper review.
| for cmd in commands: | ||
| print(color(f" $ {cmd}", Colors.DIM)) | ||
| proc = subprocess.run(cmd, cwd=str(cwd), shell=True) | ||
| try: |
There was a problem hiding this comment.
On Windows, an npm-style bootstrap command now reaches subprocess as bare list argv. hermes_cli/_subprocess_compat.py:6-9 documents that this fails for npm's .cmd shim; please resolve the executable through the existing Windows-safe path before dropping shell=True.
|
A friendly nudge on this one. It has two approvals and is mergeable, and the underlying sink is still on main ( I opened #81367 before noticing this PR and have closed mine as a duplicate. If it helps get this over the line, I have regression tests for the new no-shell behavior ready to go as a follow-up: argv shape without |
|
suggesting changes The replayed change removes implicit shell interpretation from catalog bootstrap entries, but two issues remain before merge: POSIX tokenization breaks valid Windows executable paths, and empty entries do not stay within the documented CatalogError boundary. Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub
Security evidence:
Not checked:
Signed: GPT-5.6-luna-max in Codex |
What
Converts
shell=Truesubprocess calls toshlex.split()+shell=Falsein two config-driven code paths:hermes_cli/mcp_catalog.pyshell=True→shlex.split(cmd)cli.pyquick_commandsshell=True→shlex.split(cmd)Why
Fixes #10692. Related: #2743, #16560.
Both paths execute commands from user-editable config (config.yaml
quick_commandsand MCP catalog YAMLexternal_dependencies.check). While the user controls the input,shell=Trueis a bad pattern that enables injection if config is shared or sourced from untrusted locations.Notes
&&are no longer supported in these paths — use separate command entries instead.tools/environments/docker.pyalready uses list argv (upstream fixed this).tui_gateway/server.pyshell injection (Command injection via shell=True in tui_gateway/server.py #16560) — addressed in a separate branch.