fix(security): add SSRF guard to OpenAI image_gen _load_image_bytes - #54553
fix(security): add SSRF guard to OpenAI image_gen _load_image_bytes#54553AlexFucuson9 wants to merge 1 commit into
Conversation
_load_image_bytes() fetches user-supplied image URLs server-side without SSRF protection. A model-supplied reference_url pointing to http://169.254.169.254/... would make the gateway fetch cloud metadata endpoints. Add is_safe_url() pre-flight check matching the pattern used in yuanbao_media.download_url() (PR NousResearch#54470).
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Clean, minimal security fix that adds SSRF protection to the _load_image_bytes function.
Key observations:
- Existing utility: Uses the existing is_safe_url check from tools.url_safety, which is the right pattern rather than implementing SSRF protection from scratch.
- Clear error message: Raises ValueError with a descriptive message including the blocked URL.
- Minimal change: Only 6 lines added, no unnecessary complexity.
- No security concerns: The fix correctly prevents SSRF attacks by validating URLs before fetching.
Reviewed by Hermes Agent
|
obsolete The issue this PR closes appears to be resolved already. Please reopen with a fresh target if this still covers a distinct gap. Signed: GPT-5.5-low in Codex |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying the unguarded OpenAI image-edit source path. The premise is still valid on current main: plugins/image_gen/openai/__init__.py:137 fetches an HTTP(S) ref without a safety check, and the edit flow reaches it from model-provided sources at plugins/image_gen/openai/__init__.py:263-283.
Problems
- The proposed pre-flight-only guard does not cover redirects.
tools/url_safety.py:15-23documents that redirect targets must be re-validated, and the maintained download paths do this per hop ingateway/platforms/base.py:541-554. - No regression test is included. Existing source-loader tests in
tests/plugins/image_gen/test_openai_provider.py:127-186cover local/data inputs only.
Suggested changes
- Make the
requestsfetch redirect-safe by validating every resolved redirect target, not only the initial URL. - Add direct-private, public-success, and public-to-private-redirect tests for
_load_image_bytes().
Automated hermes-sweeper review.
| if lower.startswith(("http://", "https://")): | ||
| from tools.url_safety import is_safe_url | ||
|
|
||
| if not is_safe_url(ref): |
There was a problem hiding this comment.
This guards only the initial URL. Please also validate every redirect target before following it: tools/url_safety.py:15-23 documents that pre-flight checks alone do not prevent redirect-based SSRF, and gateway/platforms/base.py:541-554 shows the established per-hop pattern.
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Two open PRs address the same unguarded server-side fetch in _load_image_bytes() by adding an initial is_safe_url(ref) check; their diffs are identical except for the error message. Neither PR has a recorded Verify verdict, and both remain incomplete because requests.get() follows redirects without re-validating each target and neither diff adds regression tests.
Related pull requests
- #54553
related— (+6/-0) — keep open for revision: The diff blocks directly unsafe URLs but does not prevent a public URL from redirecting to a private or metadata address, and it adds no URL/redirect tests. The July 15 keep_open review confirms the gap remains on current main and supersedes the earlier obsolete claim for triage purposes, but it does not support merging the current diff. - #56035
duplicate— (+6/-0) — duplicate of #54553: It adds the same pre-flight-only guard at the same code site, with only a different error string, and therefore has the same redirect bypass and missing-test gaps. Despite the keep_open review on #56035, the complete diff shows no distinct implementation to preserve, while the contributor discussion explicitly identifies it as a duplicate of #54553.
Duplicates
#54553 and #56035 implement essentially the same six-line initial URL-safety check in plugins/image_gen/openai/__init__.py; consolidate on the earlier #54553.
Suggested consolidation
Do not merge either current diff unchanged. Keep #54553 as the canonical PR, update it to validate every redirect hop and add direct-private, public-success, and public-to-private-redirect regression tests, then merge #54553 after verification; #56035 can be closed as a duplicate despite its keep_open review because its diff contributes no distinct fix beyond #54553.
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 Dup54553 ["PRs duplicating each other"]
P54553["PR #54553 (open)"]
P56035["PR #56035 (open)"]
end
class P54553 open
class P56035 open
class P54553 target
click P54553 "https://github.com/NousResearch/hermes-agent/pull/54553"
click P56035 "https://github.com/NousResearch/hermes-agent/pull/56035"
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 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 1 kB of PR diffs, 3 kB of issue/PR text, 3 kB of discussion (5 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
|
suggesting changes The preflight-only check still permits redirect SSRF. In an exact current-main replay, the patch rejected a direct metadata URL and preserved a normal public image, but a public URL whose response resolved to Please consolidate on #70350, the newer continuation for this exact fetch path, or update this PR to use the connection-time SSRF-safe client with automatic redirects disabled, validate every redirect target under a bounded hop count, and add direct-private, public-success, and public-to-private redirect regressions. The claimed #54470 precedent already revalidates redirect hops, so copying only its initial preflight does not preserve that security invariant. Security evidence:
Not checked:
Signed: GPT-5.6-sol-xhigh in Codex |
## Summary - Harden `_load_image_bytes` so redirect hops are re-checked with `is_safe_url` (`allow_redirects=False`). - Close the gap where a pre-check alone still followed a malicious `Location` into metadata/private hosts. - Add focused regression tests. ## Salvage / credit Incomplete prior fixes NousResearch#54553 / NousResearch#56035 (pre-check without redirect hop validation).
Summary
Adds
is_safe_url()SSRF guard to_load_image_bytes()inplugins/image_gen/openai/__init__.py.Root Cause
_load_image_bytes(ref)fetches user-supplied image URLs (from thesourcesparameter of thegeneratemethod) server-side viarequests.get()without any SSRF protection. A model-suppliedreference_urlpointing tohttp://169.254.169.254/latest/meta-data/would make the gateway fetch cloud metadata endpoints.This is the same class of bug fixed in PR #54470 (yuanbao_media SSRF guard) — the OpenAI image_gen plugin was the remaining unguarded server-side URL fetch.
Fix
Add
is_safe_url()pre-flight check beforerequests.get(), matching the pattern ingateway/platforms/yuanbao_media.pyandgateway/platforms/base.py.Changed files
plugins/image_gen/openai/__init__.py: Added SSRF guard in_load_image_bytes()(6 lines)