Skip to content

security(gateway): anchor api_server MEDIA tag resolution to safe paths - #57479

Closed
srojk34 wants to merge 1 commit into
NousResearch:mainfrom
srojk34:fix/api-server-media-path-traversal
Closed

security(gateway): anchor api_server MEDIA tag resolution to safe paths#57479
srojk34 wants to merge 1 commit into
NousResearch:mainfrom
srojk34:fix/api-server-media-path-traversal

Conversation

@srojk34

@srojk34 srojk34 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

_resolve_media_to_data_urls (added by #56959, feat(api-server): inline MEDIA: image tags as base64 data URLs for remote frontends) scans the model's own final response text for MEDIA:<path> tags and inlines matching local images as base64 data URLs, since remote OpenAI-compatible frontends can't read local file paths.

Its matcher, _MEDIA_TAG_RE, had no absolute-path anchor — it matched any non-whitespace token (or quoted string) after MEDIA:, including a relative/traversal path like MEDIA:../../../../etc/passwd.png. The resolved path was then read directly with Path(...).expanduser() + is_file() + a 5MB size cap and no denylist at all.

This recreates, from scratch, a vulnerability class this codebase has fixed independently at least 8 times on other platforms (issue #16721 and others). Every other platform adapter's MEDIA: handling already goes through two shared, hardened primitives in gateway/platforms/base.py:

  • MEDIA_TAG_CLEANUP_RE — anchors the path to ~/, /, or a Windows drive letter (X:\/X:/), plus a known deliverable extension, with a proper trailing terminator.
  • validate_media_delivery_path — resolves symlinks (Path.resolve(strict=True)), requires an absolute path, and rejects anything under the credential/system-path denylist (/etc, /proc, ~/.ssh, ~/.aws, Hermes credential stores, etc.) unless it's under an operator-allowlisted root.

Impact before this fix: any image-suffixed file the hermes process could read — reachable via a relative path, or an absolute credential path if it happened to have (or was renamed/symlinked to have) an allowed extension — could be exfiltrated as base64 to a remote API client if its path merely appeared in the model's own reply text (e.g. via prompt injection, an echoed example string, or stale tool output).

Fix

Replace the local _MEDIA_TAG_RE pattern and naive path handling in _to_data_url with the same MEDIA_TAG_CLEANUP_RE / validate_media_delivery_path pair every other adapter uses. The existing _MEDIA_IMG_EXT / _MEDIA_MIME maps are kept as-is — they narrow the broader MEDIA_DELIVERY_EXTS allowlist down to the specific image types this function inlines (PDFs/videos/etc. still correctly fall through untouched, matching current behavior).

Test plan

  • Live PoC against current main: absolute non-denylisted path inlines correctly; MEDIA:../../../../tmp/x/secret.png is left as literal text (not absolute, regex doesn't match); MEDIA:~/.ssh/id_rsa.png is left as literal text (denylist rejects it).
  • Added test_relative_traversal_path_not_inlined, test_credential_path_not_inlined_even_with_image_extension, and test_symlink_escaping_to_denylisted_target_not_inlined (symlink target under /etc must not be inlined — validate_media_delivery_path resolves symlinks before the denylist check) to tests/gateway/test_api_server_media_data_urls.py.
  • uv run --frozen --extra dev --extra messaging python -m pytest tests/gateway/test_api_server_media_data_urls.py -q — all 10 tests (7 existing + 3 new) pass.
  • uv run --frozen --extra dev --extra messaging python -m pytest tests/gateway/ -k "api_server or media" -q — 709 passed, 4 skipped, 1 pre-existing unrelated failure (test_background_command.py::test_media_files_routed_by_type, a /var vs /private/var macOS symlink assertion in an unrelated file — confirmed present on a clean main checkout before this change, not a regression).
  • ruff check on both changed files — clean.

_resolve_media_to_data_urls's ad-hoc _MEDIA_TAG_RE matched any bare
token after MEDIA: (no absolute-path anchor) and read the resolved
path directly with no denylist. A relative/traversal path like
MEDIA:../../../../etc/passwd.png slipped through, and any image-
suffixed file the process could read (including under ~/.ssh, ~/.aws,
etc.) was base64-inlined into the API response if its path merely
appeared in the model's own final reply text.

Every other platform adapter's MEDIA: handling already goes through
two shared primitives in gateway/platforms/base.py:
  - MEDIA_TAG_CLEANUP_RE, which anchors the path to ~/, /, or a
    Windows drive letter plus a known deliverable extension.
  - validate_media_delivery_path, which resolves symlinks and rejects
    paths under the credential/system-path denylist.

Reuse both here instead of the local unanchored pattern and naive
Path().expanduser() resolution.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery 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
@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 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Merged via PR #57660 — your commit was cherry-picked onto current main with your authorship preserved in git log (rebase-merge). Thanks for the fix!

@teknium1 teknium1 closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P1 High — major feature broken, no workaround sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants