Skip to content

fix(security): add SSRF guard to OpenAI image_gen _load_image_bytes - #54553

Open
AlexFucuson9 wants to merge 1 commit into
NousResearch:mainfrom
AlexFucuson9:fix/openai-image-gen-ssrf
Open

fix(security): add SSRF guard to OpenAI image_gen _load_image_bytes#54553
AlexFucuson9 wants to merge 1 commit into
NousResearch:mainfrom
AlexFucuson9:fix/openai-image-gen-ssrf

Conversation

@AlexFucuson9

Copy link
Copy Markdown
Contributor

Summary

Adds is_safe_url() SSRF guard to _load_image_bytes() in plugins/image_gen/openai/__init__.py.

Root Cause

_load_image_bytes(ref) fetches user-supplied image URLs (from the sources parameter of the generate method) server-side via requests.get() without any SSRF protection. A model-supplied reference_url pointing to http://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 before requests.get(), matching the pattern in gateway/platforms/yuanbao_media.py and gateway/platforms/base.py.

Changed files

  • plugins/image_gen/openai/__init__.py: Added SSRF guard in _load_image_bytes() (6 lines)

_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).
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/plugins Plugin system and bundled plugins tool/vision Vision analysis and image generation provider/openai OpenAI / Codex Responses API sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jun 29, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@egilewski

Copy link
Copy Markdown
Contributor

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 teknium1 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.

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-23 documents that redirect targets must be re-validated, and the maintained download paths do this per hop in gateway/platforms/base.py:541-554.
  • No regression test is included. Existing source-loader tests in tests/plugins/image_gen/test_openai_provider.py:127-186 cover local/data inputs only.

Suggested changes

  • Make the requests fetch 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):

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.

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.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 15, 2026

@GottZ GottZ 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.

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"
Loading

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.

@egilewski

Copy link
Copy Markdown
Contributor

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 169.254.169.254 still returned the synthetic metadata body. Only the original public URL was passed to is_safe_url; requests.get(..., timeout=60) follows redirects by default and this path never validates the resolved hop.

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:

  • trust boundary: model-supplied image-edit URL -> Hermes-owned server-side HTTP fetch
  • source/sink/invariant: _load_image_bytes() passes the URL to Requests; every connected destination and redirect hop must remain outside private and metadata space
  • current-main reproduction: direct metadata and public-to-metadata redirect cases returned synthetic response bodies
  • PR-head or patch-replay validation: direct metadata was blocked, public success remained usable, and the redirect case still returned SYNTHETIC_METADATA_SECRET
  • positive/negative cases: direct-private denied; public image allowed; public-to-private redirect not denied
  • residual bypass search: only the initial URL is checked; no redirect or final-response URL is validated
  • reviewer validation: exact current-main replay; 20 unaffected focused provider tests passed on both clean main and the replay

Not checked:

  • Full test suite
  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

zapabob added a commit to zapabob/hermes-agent-windows that referenced this pull request Aug 23, 2026
## 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists provider/openai OpenAI / Codex Responses API sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/vision Vision analysis and image generation type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants