Skip to content

fix(image-gen): guard local provider inputs against credential reads - #57698

Closed
necoweb3 wants to merge 1 commit into
NousResearch:mainfrom
necoweb3:fix/image-provider-local-input-read-guard
Closed

fix(image-gen): guard local provider inputs against credential reads#57698
necoweb3 wants to merge 1 commit into
NousResearch:mainfrom
necoweb3:fix/image-provider-local-input-read-guard

Conversation

@necoweb3

@necoweb3 necoweb3 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

OpenAI and OpenRouter image-generation providers accepted local image_url / reference_image_urls inputs and read those paths before sending the bytes to the upstream provider.

Those local input paths did not use Hermes' existing agent.file_safety.get_read_block_error() guard. As a result, a model/tool-supplied path to a Hermes credential store could be read as source image input and leave the machine in the provider request.

The OpenAI Codex image provider already guards this same local-image path; this applies the same read boundary to the OpenAI and OpenRouter image providers.

Changes

  • Guard OpenAI image edit local source reads with get_read_block_error().
  • Guard OpenRouter local reference-image inlining with get_read_block_error().
  • Add regression coverage for ~/.hermes/auth.json being rejected before local bytes are read/inlined.

Security impact

This prevents image-generation reference inputs from bypassing Hermes' credential-file read denylist and sending local credential stores to external image providers.

Tests

python -m pytest tests/plugins/image_gen/test_openrouter_compat_provider.py -k "to_image_url_part" -q --timeout-method=thread
4 passed, 32 deselected

python -m pytest tests/plugins/image_gen/test_openai_provider.py -k "SourceImageLoading or b64_saves_to_cache" -q --timeout-method=thread
2 passed, 24 deselected

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/plugins Plugin system and bundled plugins tool/vision Vision analysis and image generation provider/openai OpenAI / Codex Responses API provider/openrouter OpenRouter aggregator P1 High — major feature broken, no workaround sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 3, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #57695 (same credential-read guard applied to the xAI image/video providers). This PR extends the same get_read_block_error() boundary to the OpenAI and OpenRouter image providers — complementary sibling fixes, not duplicates.

@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

The local image input paths for OpenAI and OpenRouter now call agent.file_safety.get_read_block_error() before local reads, propagate the credential-store ValueError, and keep remote/data references unchanged; I checked the image_url, reference_image_urls, and OpenRouter reference_images source paths, the OpenAI multipart and OpenRouter data-URI sinks, current-main reproduction against PR-head validation, focused positive/negative provider tests, ruff check --no-cache on the touched files, sibling-provider coverage, clean merge state against current GitHub main, and CodeRabbit's major guard-failure finding. CodeRabbit's finding is not a posting blocker here: the credential-store denial path itself raises ValueError and is re-raised before reads, the guard module is part of the same Hermes runtime these providers already import, and the remaining best-effort exception path matches the existing openai-codex image input guard rather than weakening the newly covered credential-store case.

Signed: GPT-5.5-xhigh in Codex

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Superseded by #57726, which salvages this fix (your commit cherry-picked, authorship preserved) and extends it:

  • Whole bug class: the xAI image provider (_xai_image_field) had the identical unguarded local read this PR missed — now covered.
  • Single shared chokepoint: extracted agent.file_safety.raise_if_read_blocked() so OpenAI, OpenRouter, and xAI enforce the identical boundary in one place instead of duplicated inline try/except.
  • Stronger tests: no-read spies (assert the credential is never opened/read) + negative controls (legit local image loads; remote/data: URIs pass through) across all three providers.

Verified: 225 tests pass, mutation-checked, and E2E against a real profile-scoped HERMES_HOME blocks auth.json / .anthropic_oauth.json in all three providers while a legit image loads.

Thanks for the fix and the clean diagnosis — credit preserved in #57726's history. Closing as superseded.

kshitijk4poor added a commit that referenced this pull request Jul 3, 2026
… chokepoint + cover xai (#57698)

Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Merged via #57726 — your commit is cherry-picked with authorship preserved (587be5b5b on main is authored by you).

Thanks for catching this credential-read gap. On top of your openai + openrouter fix, the salvage extended it to the whole bug class: covered the xAI image provider (which had the identical unguarded open(...).read()), routed all providers through a single shared agent.file_safety.raise_if_read_blocked() chokepoint instead of per-provider copy-paste, and upgraded the tests from pass-on-any-ValueError to real security invariants (no-read spies + negative controls + symlink→auth.json regression tests).

#57726

teknium1 pushed a commit that referenced this pull request Jul 5, 2026
…al-read guard

video_analyze_tool's local-path branch read raw bytes via
_detect_video_mime_type (extension-only, no magic-byte check) with no
call to agent.file_safety.raise_if_read_blocked, unlike the image-gen
and video-gen provider plugins that already route local inputs through
that shared chokepoint (#57698). A model could point video_url at a
credential store (e.g. .env, auth.json) renamed or symlinked to a
video-like extension and have its raw bytes base64-encoded and sent to
the vision provider.

vision_analyze_tool and its native fast path (_vision_analyze_native)
had the same gap in their local-file branches; they were only
incidentally protected by the image magic-byte sniff rejecting
non-image content, not by the intended read guard.

Add raise_if_read_blocked() to all three local-file branches, mirroring
the existing plugins/image_gen and plugins/video_gen call sites.
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
… chokepoint + cover xai (NousResearch#57698)

Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…al-read guard

video_analyze_tool's local-path branch read raw bytes via
_detect_video_mime_type (extension-only, no magic-byte check) with no
call to agent.file_safety.raise_if_read_blocked, unlike the image-gen
and video-gen provider plugins that already route local inputs through
that shared chokepoint (NousResearch#57698). A model could point video_url at a
credential store (e.g. .env, auth.json) renamed or symlinked to a
video-like extension and have its raw bytes base64-encoded and sent to
the vision provider.

vision_analyze_tool and its native fast path (_vision_analyze_native)
had the same gap in their local-file branches; they were only
incidentally protected by the image magic-byte sniff rejecting
non-image content, not by the intended read guard.

Add raise_if_read_blocked() to all three local-file branches, mirroring
the existing plugins/image_gen and plugins/video_gen call sites.
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
… chokepoint + cover xai (NousResearch#57698)

Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…al-read guard

video_analyze_tool's local-path branch read raw bytes via
_detect_video_mime_type (extension-only, no magic-byte check) with no
call to agent.file_safety.raise_if_read_blocked, unlike the image-gen
and video-gen provider plugins that already route local inputs through
that shared chokepoint (NousResearch#57698). A model could point video_url at a
credential store (e.g. .env, auth.json) renamed or symlinked to a
video-like extension and have its raw bytes base64-encoded and sent to
the vision provider.

vision_analyze_tool and its native fast path (_vision_analyze_native)
had the same gap in their local-file branches; they were only
incidentally protected by the image magic-byte sniff rejecting
non-image content, not by the intended read guard.

Add raise_if_read_blocked() to all three local-file branches, mirroring
the existing plugins/image_gen and plugins/video_gen call sites.
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
… chokepoint + cover xai (NousResearch#57698)

Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…al-read guard

video_analyze_tool's local-path branch read raw bytes via
_detect_video_mime_type (extension-only, no magic-byte check) with no
call to agent.file_safety.raise_if_read_blocked, unlike the image-gen
and video-gen provider plugins that already route local inputs through
that shared chokepoint (NousResearch#57698). A model could point video_url at a
credential store (e.g. .env, auth.json) renamed or symlinked to a
video-like extension and have its raw bytes base64-encoded and sent to
the vision provider.

vision_analyze_tool and its native fast path (_vision_analyze_native)
had the same gap in their local-file branches; they were only
incidentally protected by the image magic-byte sniff rejecting
non-image content, not by the intended read guard.

Add raise_if_read_blocked() to all three local-file branches, mirroring
the existing plugins/image_gen and plugins/video_gen call sites.
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
… chokepoint + cover xai (NousResearch#57698)

Follow-up to the per-provider guards. Three improvements from review:

1. Extract agent.file_safety.raise_if_read_blocked() as a single shared
   chokepoint and route the OpenAI, OpenRouter, and (newly) xAI image
   providers through it, replacing the 3x-duplicated inline try/except.
   Fixes the whole bug class: xai/_xai_image_field read a model-supplied
   local path via open() with no guard — the same vulnerability the PR
   fixed for OpenAI/OpenRouter, in a sibling provider it missed.
2. Strengthen the regression tests from pass-on-any-ValueError to true
   security invariants: spy open()/read_bytes() and assert the blocked
   credential is NEVER read; add negative controls (legit local image
   still loads; remote/data: URIs pass through unguarded) so a
   block-everything regression can't pass.
3. Guard is best-effort by design (defense-in-depth, not a security
   boundary) — documented on the shared helper.

- agent/file_safety.py: raise_if_read_blocked()
- plugins/image_gen/{openai,openrouter,xai}: route through helper
- tests: no-read spies + negative controls across all three providers
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…al-read guard

video_analyze_tool's local-path branch read raw bytes via
_detect_video_mime_type (extension-only, no magic-byte check) with no
call to agent.file_safety.raise_if_read_blocked, unlike the image-gen
and video-gen provider plugins that already route local inputs through
that shared chokepoint (NousResearch#57698). A model could point video_url at a
credential store (e.g. .env, auth.json) renamed or symlinked to a
video-like extension and have its raw bytes base64-encoded and sent to
the vision provider.

vision_analyze_tool and its native fast path (_vision_analyze_native)
had the same gap in their local-file branches; they were only
incidentally protected by the image magic-byte sniff rejecting
non-image content, not by the intended read guard.

Add raise_if_read_blocked() to all three local-file branches, mirroring
the existing plugins/image_gen and plugins/video_gen call sites.
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 P1 High — major feature broken, no workaround provider/openai OpenAI / Codex Responses API provider/openrouter OpenRouter aggregator 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.

4 participants