Skip to content

fix(security): validate cron job base_url against SSRF with fail-closed ImportError handling#10180

Open
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/cron-base-url-ssrf
Open

fix(security): validate cron job base_url against SSRF with fail-closed ImportError handling#10180
memosr wants to merge 1 commit into
NousResearch:mainfrom
memosr:fix/cron-base-url-ssrf

Conversation

@memosr

@memosr memosr commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Validates base_url supplied by cron jobs against SSRF using the
existing is_safe_url() helper, with fail-closed behavior if the
module cannot be imported.

SSRF protection

Cron jobs can supply a custom base_url override. Without validation,
a crafted URL like http://169.254.169.254/latest/meta-data/ could
leak API keys to AWS IMDS or internal services.

Fix: Validates _job_base_url against is_safe_url() before use.

Fail-closed ImportError handling

Previous version used except ImportError: pass — silently disabling
the security check if tools.url_safety failed 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 ImportError now calls logger.error() explicitly
and sets _is_safe_url = None — no job may supply a custom base_url
if the module fails to load.

except ImportError:
    logger.error(
        "tools.url_safety could not be imported; job base_url overrides are "
        "disabled (fail-closed SSRF protection)"
    )
    _is_safe_url = None
    _job_base_url = None

Type of Change

  • 🔒 Security fix (SSRF)
  • 🔒 Security fix (fail-closed ImportError handling)

Checklist

  • Read the Contributing Guide
  • Commit messages follow Conventional Commits
  • Addresses @trevorgordon981 review feedback
  • Fail-closed — safer default when security module unavailable

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/cron Cron scheduler and job management labels Apr 26, 2026
memosr added a commit to memosr/hermes-agent that referenced this pull request Jun 5, 2026
…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 egilewski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 main and git 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 in cron/scheduler.py while also auto-merging gateway/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

@memosr memosr force-pushed the fix/cron-base-url-ssrf branch from 6fd701d to 5a2b5b0 Compare June 14, 2026 18:55
@memosr

memosr commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@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.
The branch now carries a single commit against main; an unrelated, already-upstreamed commit (WeChat media SSRF) was dropped by the rebase as expected. The cron base_url protection is intact: _is_safe_url is defined fail-closed (disabled on ImportError) and used to gate the job-supplied base_url in run_job. CI is re-running - ready for the convention screen + focused security probes whenever you are.

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

Labels

comp/cron Cron scheduler and job management P1 High — major feature broken, no workaround type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants