fix(security): validate cron job base_url against SSRF with fail-closed ImportError handling#10180
fix(security): validate cron job base_url against SSRF with fail-closed ImportError handling#10180memosr wants to merge 1 commit into
Conversation
…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.
egilewski
left a comment
There was a problem hiding this comment.
Recommendation: request changes.
I reviewed this in security mode against current GitHub main 7b9dc7cd0a489230a70f5876492b47e43686bca1, PR base e69526be799edcfa02c25bf8966af2d8cce65ee3, and PR head 6fd701de62323f6e6bf09383cef9e014921e121a.
Validation:
git fetch --no-tags upstream mainandgit fetch --no-tags upstream +pull/10180/head:refs/remotes/upstream/pr/10180: passed; resolved current main and PR head to the SHAs above.git rev-list --left-right --count refs/remotes/upstream/main...refs/remotes/upstream/pr/10180:7268 2, so the branch is very stale against current main.git diff --check refs/remotes/upstream/main...refs/remotes/upstream/pr/10180: passed.git merge-tree --write-tree refs/remotes/upstream/main refs/remotes/upstream/pr/10180: failed with a content conflict incron/scheduler.pywhile also auto-merginggateway/platforms/weixin.py.
Finding:
The submitted branch does not currently merge with GitHub main. Please rebase/update the PR and resolve the cron/scheduler.py conflict before deeper validation; I stopped at this mergeability gate, so I did not run the convention screen or focused security probes.
Signed: GPT-5.5-xhigh in Codex
6fd701d to
5a2b5b0
Compare
|
@egilewski Rebased onto current main and resolved the cron/scheduler.py conflict. It was a co-location collision: main added CronPromptInjectionBlocked + the resolve_cron*_toolsets helpers in the same top-of-module region where this PR adds the SSRF import guard / kept both, no logic dropped from either side. |
What does this PR do?
Validates
base_urlsupplied by cron jobs against SSRF using theexisting
is_safe_url()helper, with fail-closed behavior if themodule cannot be imported.
SSRF protection
Cron jobs can supply a custom
base_urloverride. Without validation,a crafted URL like
http://169.254.169.254/latest/meta-data/couldleak API keys to AWS IMDS or internal services.
Fix: Validates
_job_base_urlagainstis_safe_url()before use.Fail-closed ImportError handling
Previous version used
except ImportError: pass— silently disablingthe security check if
tools.url_safetyfailed to import.As pointed out by @trevorgordon981, this means any import failure
(missing module, circular import, SyntaxError in a dependency) would
run the original vulnerable code path with zero log output.
Fix:
except ImportErrornow callslogger.error()explicitlyand sets
_is_safe_url = None— no job may supply a custombase_urlif the module fails to load.
Type of Change
Checklist