fix(security): block SSRF in yuanbao download_url - #43938
Conversation
download_url() fetched arbitrary URLs that originate from model/agent output (e.g. an  image link the gateway resolves and sends back). With no validation, a crafted URL could make the gateway hit internal services or the cloud metadata endpoint (169.254.169.254), and even a public URL could 30x-redirect to an internal target. - Reject private/internal/metadata targets up front via is_safe_url. - Re-validate every redirect hop with an httpx response event hook so a 30x redirect to an internal address is blocked mid-flight. - Truncate URLs in error messages to avoid leaking long query strings. Adds regression tests covering private-target rejection and the redirect guard (block + allow + non-redirect no-op). Co-authored-by: Cursor <cursoragent@cursor.com>
|
Verification review — reviewed the diff for SSRF protection quality. The implementation is solid:
One note: the guard only applies to the |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Security: Block SSRF in yuanbao download_url
- Problem:
download_urlhad no SSRF protection. The URL originates from model/agent output (e.g.image link in a reply), so it is attacker-influenceable. A malicious URL could redirect to169.254.169.254(cloud metadata) or127.0.0.1. - Fix: Two-layer defense:
is_safe_url()check before making the request (blocks private/internal targets outright)._ssrf_redirect_guardhook re-validates every redirect hop — prevents bypass of step 1 via a public URL that 30x-redirects to a private target.
- Logging safety:
_safe_url_for_log()truncates long URLs to avoid leaking sensitive query strings in logs. - Tests: 5 cases blocking private targets, redirect guard blocks unsafe redirect, allows safe redirect, no-op on non-redirect.
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused Yuanbao SSRF hardening. The pre-flight URL check is already on current main (gateway/platforms/yuanbao_media.py:220-226), but the redirect portion cannot be salvaged unchanged.
Problems
- The proposed guard at
gateway/platforms/yuanbao_media.py:48relies onresponse.next_request.tools/url_safety.py:479-503documents that this is frequentlyNoneinside HTTPX response hooks, so an unsafeLocationredirect can still be followed. - The synthetic redirect tests always populate
next_request(tests/gateway/platforms/test_yuanbao_media_ssrf.py:29-35), and therefore miss that runtime case.
Suggested changes
- Reuse
gateway.platforms.base._ssrf_redirect_guard(gateway/platforms/base.py:541-554), which callsredirect_target_from_response()and validatesLocationtargets. - Add an HTTPX MockTransport regression for a public URL redirecting via
Locationto a private address, asserting the private request is not issued.
This is an automated hermes-sweeper review.
| Without this, an attacker can host a public URL that 30x-redirects to | ||
| http://169.254.169.254/ (cloud metadata) or http://127.0.0.1/ and bypass | ||
| the pre-flight ``is_safe_url`` check on the initial URL. | ||
| """ |
There was a problem hiding this comment.
response.next_request is frequently None inside HTTPX response event hooks, so this condition can skip a real Location redirect entirely. Use the shared redirect_target_from_response() path (via gateway.platforms.base._ssrf_redirect_guard) so relative and absolute Location targets are checked before HTTPX follows them.
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Four PRs address or reference the Yuanbao SSRF chain: #43938 and merged #54470 add the initial URL check plus a redirect hook, merged #56160 fixes the shared response.next_request redirect-guard failure across other download paths, and #56927 applies that proven fix to the Yuanbao path missed by #56160.
Related pull requests
- #43938
related— (+109/-1) — close as superseded duplicate: Like #54470, this adds a Yuanbao pre-flight check and a redirect hook, but its hook relies onresponse.next_requestand its synthetic tests always populate that field, missing the actual HTTPX hook behavior. Despite the keep_open review on #43938, the diff retains the documented bypass and current main already contains the pre-flight protection from #54470; #56927 supplies the correctLocation-based follow-up. - #54470 [merged]
duplicate— (+113/-1) — merged foundation: This remains relevant as the merged implementation that introduced Yuanbao pre-flight SSRF validation, but itsresponse.next_request-based redirect hook is ineffective inside HTTPX response hooks and is corrected by #56927. - #56160 [merged]
related— (+111/-26) — merged reference implementation: This fixes the same redirect-hook root cause across the shared gateway, Slack, and vision download paths by introducingredirect_target_from_response(), but its diff does not updateyuanbao_media.py; #56927 completes that missed site. - #56927
related— (+80/-7) — merge: It replaces Yuanbao's deadresponse.next_requestcondition with the merged #56160Location-aware helper, preserves the newer DNS-pinned SSRF-safe client, and adds a regression for a metadata redirect withnext_request=None. This matches the keep_open review on #56927; after DNS pinning it restores layered application-level rejection rather than closing a currently exploitable connect-time bypass.
Duplicates
#43938 and #54470 substantially implement the same initial Yuanbao pre-flight check and response.next_request redirect hook; #54470 is already merged. #56927 is not a duplicate of #56160 but the Yuanbao completion of its root-cause fix.
Suggested consolidation
Merge #56927 because it applies the established #56160 redirect-target helper to the remaining Yuanbao hook while retaining DNS pinning. Close #43938 as superseded by merged #54470 plus #56927; keep merged #54470 and #56160 as the foundation and reference implementation, respectively.
Complex graph
flowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
subgraph Dup43938 ["PRs duplicating each other"]
P43938["PR #43938 (open)"]
P54470["PR #54470 (merged)"]
end
class P43938 open
class P54470 merged
class P43938 target
click P43938 "https://github.com/NousResearch/hermes-agent/pull/43938"
click P54470 "https://github.com/NousResearch/hermes-agent/pull/54470"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed or no verify verdict yet (state tag in the node label).
Cross-PR triage: Reviewed 4 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 23 kB of PR diffs, 10 kB of issue/PR text, 7 kB of discussion (9 comments), 2 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
Summary
gateway/platforms/yuanbao_media.py::download_url()fetched arbitrary URLswith no SSRF protection. The URL is attacker-influenceable: it originates
from model/agent output (e.g. an
image link in a reply that theYuanbao gateway resolves and downloads to re-upload). A crafted URL could make
the gateway issue requests to:
http://169.254.169.254/…) to steal IAM creds,http://127.0.0.1:…),http://10.0.0.0/8, etc.).Because
follow_redirects=Truewas set with no per-hop check, even a publicURL could 30x-redirect to an internal target and bypass any naive front check.
Fix
tools.url_safety.is_safe_urlhelper (the same one used elsewhere in thegateway, e.g.
gateway/platforms/base.py).httpxresponse event hook(
_ssrf_redirect_guard) so a redirect to an internal address is blockedmid-flight.
No behavior change for legitimate public media URLs.
Test plan
tests/gateway/platforms/test_yuanbao_media_ssrf.py— new regression tests:169.254.169.254,127.0.0.1,localhost,::1,10.0.0.5)no-op on non-redirect responses