Skip to content

fix(security): fail-closed when dangerous-command guard cannot be loaded in TUI shell.exec#15542

Open
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/tui-gateway-shell-exec-guard
Open

fix(security): fail-closed when dangerous-command guard cannot be loaded in TUI shell.exec#15542
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/tui-gateway-shell-exec-guard

Conversation

@memosr

@memosr memosr commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

tui_gateway/server.py exposes a shell.exec JSON-RPC method that
runs shell commands via subprocess.run(shell=True). It tries to
block dangerous commands using tools.approval.detect_dangerous_command,
but on ImportError it silently falls through to executing the command:

# Before (vulnerable — fail-open)
try:
    from tools.approval import detect_dangerous_command
    is_dangerous, _, desc = detect_dangerous_command(cmd)
    if is_dangerous:
        return _err(rid, 4005, f"blocked: {desc}...")
except ImportError:
    pass  # ← silently disables the guard

If tools.approval cannot be imported (missing module, circular import,
SyntaxError in a dependency, partial deployment), the dangerous-command
check is silently skipped and arbitrary shell commands are executed.

Fix

Made the handler fail-closed: if the guard cannot be loaded, refuse
to execute the command rather than silently bypassing the check:

except ImportError:
    return _err(
        rid, 4006, "dangerous-command guard unavailable; refusing to execute"
    )

This is consistent with the fail-closed pattern applied to the cron
SSRF check in #10180.

Type of Change

  • 🔒 Security fix (CRITICAL — fail-open command execution)

Checklist

  • Read the Contributing Guide
  • Commit messages follow Conventional Commits
  • Fail-closed — safer default when security module unavailable
  • No behavior change when tools.approval is available

@egilewski

Copy link
Copy Markdown
Contributor

Security behavior looks correct; I would merge once the required CI test check is green.

I validated this against current upstream/main b1e399de95894643c4d67105cd3bd84746c211b8 and PR head 0a55150e1cc3f782b3d35a9b24627e16c214b06b.

Evidence:

  • Reproduced the current-main fail-open path with a synthetic ImportError for tools.approval and a mocked subprocess.run: shell.exec continued to the subprocess path.
  • Replayed the same probe on this PR patch: shell.exec returned error 4006 and made zero subprocess calls.
  • Verified the normal guard path still blocks dangerous commands and still allows safe commands through when tools.approval imports successfully.
  • Ran python -m compileall -q tui_gateway/server.py on both current main and the patched worktree.
  • Ran python -m pytest -o addopts='' -p no:cacheprovider tests/test_tui_gateway_server.py -q on the patched worktree: 205 passed, 1 warning.
  • Ran coderabbit review --plain --base b1e399de95894643c4d67105cd3bd84746c211b8 --type uncommitted: No findings.

GitHub currently reports the PR as MERGEABLE / BLOCKED; the required test check is failing, so that check still needs to be rerun or fixed before merge.

Signed: GPT-5.5-xhigh in Codex

…ded in TUI shell.exec

tui_gateway/server.py exposes a shell.exec JSON-RPC method that
runs shell commands via subprocess.run(shell=True). It tries to
block dangerous commands using tools.approval.detect_dangerous_command,
but on ImportError silently falls through to executing the command:

    try:
        from tools.approval import detect_dangerous_command
        is_dangerous, _, desc = detect_dangerous_command(cmd)
        if is_dangerous:
            return _err(rid, 4005, f'blocked: {desc}...')
    except ImportError:
        pass    # <-- silently disables the guard, then runs anyway

If tools.approval cannot be imported (missing module, circular
import, syntax error in a dependency, partial deploy), the screener
is skipped and arbitrary shell commands reach subprocess.run.

Fix:

* Replace the silent ImportError pass with a fail-closed return:
  error code 4006 with message 'dangerous-command guard unavailable;
  refusing to execute'. Consistent with the fail-closed pattern
  applied to the cron SSRF check in NousResearch#10180 and the WebSocket
  empty-peer guard in NousResearch#15544.

No behavior change when tools.approval is available -- the existing
detect_dangerous_command path runs unchanged, dangerous commands
still return 4005, and safe commands still flow through to
subprocess.run.

Regression tests added in tests/test_tui_gateway_server.py:

* test_shell_exec_fails_closed_when_guard_unavailable -- injects an
  ImportError for tools.approval via a patched builtins.__import__
  and a tripwire on subprocess.run; asserts the response is error
  4006 with 'guard unavailable' in the message and that
  subprocess.run is never reached.

* test_shell_exec_still_runs_when_guard_available -- guards against
  over-reach: with tools.approval present and the command not
  flagged dangerous, shell.exec must still execute. Without this
  the fail-closed change could regress into 'refuses everything'.

Verified locally that the fail-closed test fails without this
patch (the response carries error code 5003 from the tripwire-raised
AssertionError inside subprocess.run, proving the silent-skip path
was reached) and passes with the fix in place. The 205 pre-existing
tests in test_tui_gateway_server.py are unaffected by this change.

Per @egilewski's audit on this PR (NousResearch#15542), the original April patch
targeted the same fail-open path; the file has refactored heavily
since (tui_gateway/server.py is now ~8k lines vs ~4.6k at the
original PR head), so this commit re-applies the fail-closed
behavior at the current shell.exec call site and adds the
regression coverage that the original PR was missing.
@memosr memosr force-pushed the fix/tui-gateway-shell-exec-guard branch from 0a55150 to 0e8648c Compare June 5, 2026 14:54
magnus919 added a commit to magnus919/hermes-agent-1 that referenced this pull request Jun 7, 2026
…guarded_command

The quick-commands exec path (command.dispatch) ran bare
subprocess.run(..., shell=True) with no dangerous-command detection.

This was already partially addressed upstream for shell.exec (fail-closed
ImportError pattern). This change:

1. Adds _exec_guarded_command() — a reusable helper that blocks known
   dangerous patterns, fails closed on ImportError, and returns structured
   result dicts with uniform error handling
2. Refactors the quick-commands exec path to use it instead of bare
   subprocess.run(shell=True)

The shell.exec handler was already fixed upstream and is not re-touched.

Tested: 5/6 local proof tests pass (6th is pre-existing CI issue).
169 tests pass in test_tui_gateway_server.py (1 pre-existing failure in
browser_manage unrelated to this change). All 9 command/shell/exec/dispatch
tests pass.

Refs NousResearch#16560
Supersedes NousResearch#15542 (still open)
@magnus919

Copy link
Copy Markdown

Closing as superseded by upstream changes + PR #28214.

No action needed on this PR.

@magnus919

Copy link
Copy Markdown

Closing — superseded by upstream changes to shell.exec + PR #28214 for quick-commands hardening.

@egilewski

egilewski commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Recommendation: close as superseded by current upstream changes.

I rechecked this against current upstream/main c3055d61857751ad82a2bf9e4f5de5d26a8f2a16 and PR head 0e8648cf74014e121d5b5fff2967891bbb6ebef6.

Evidence:

  • Current upstream already makes shell.exec fail closed when tools.approval cannot be imported, returning 5001 before final shell execution.
  • Current upstream also adds hardline command detection before the existing dangerous-command detector.
  • This PR is now 322 commits behind upstream/main, and git merge-tree c3055d61857751ad82a2bf9e4f5de5d26a8f2a16 0e8648cf74014e121d5b5fff2967891bbb6ebef6 reports conflicts in tui_gateway/server.py and tests/test_tui_gateway_server.py.
  • git diff --check upstream/main...HEAD reports a trailing blank line at EOF in tests/test_tui_gateway_server.py.
  • Focused probe on current upstream with simulated ImportError for tools.approval: error response, zero final shell execution calls.
  • Focused probe on this PR head: error 4006, zero final shell execution calls.
  • /home/mac/hermes-agent/.venv/bin/python -m compileall -q tui_gateway/server.py tests/test_tui_gateway_server.py: passed.
  • PR worktree focused test run with /home/mac/hermes-agent/.venv/bin/python -m pytest -o addopts="" -p no:cacheprovider tests/test_tui_gateway_server.py -q: 207 passed, 1 warning.

The regression test coverage added here is still useful, but the implementation is stale and conflicts with the newer upstream handler. I would close this PR as superseded rather than merge it as-is.

Signed: GPT-5.5-xhigh in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P0 Critical — data loss, security, crash loop type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants