Skip to content

[security] fix(gateway): contain MEDIA directive file delivery#16547

Closed
Hinotoi-agent wants to merge 5 commits into
NousResearch:mainfrom
Hinotoi-agent:fix/gateway-media-directive-containment
Closed

[security] fix(gateway): contain MEDIA directive file delivery#16547
Hinotoi-agent wants to merge 5 commits into
NousResearch:mainfrom
Hinotoi-agent:fix/gateway-media-directive-containment

Conversation

@Hinotoi-agent

@Hinotoi-agent Hinotoi-agent commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR hardens the gateway MEDIA[:]<path> attachment path against model-controlled local-file exfiltration by requiring every extracted media directive to resolve to an existing regular file under a Hermes-managed media cache root, or under an operator-configured allowlist root.

The current gateway treats assistant output as an attachment instruction: if a response contains the media-delivery prefix followed by an absolute local path, the platform adapter extracts that path and later sends the referenced local file through Telegram/Discord/Slack/etc. Because assistant text can be influenced by untrusted prompts, arbitrary host paths should not become sendable attachments.

Security issues covered

Issue Severity Status
Model-controlled MEDIA[:] directives can cause native gateway delivery of arbitrary local files, including sensitive extensionless host files High Fixed by path existence, regular-file, symlink-resolved containment checks

Before this PR

  • BasePlatformAdapter.extract_media() accepted absolute MEDIA[:] paths after simple parsing and ~ expansion.
  • Any readable host file that matched the parser's delivery handling could be queued for native platform delivery, including extensionless paths in observed gateway behavior.
  • The check did not distinguish files generated by Hermes from unrelated files on the host.
  • A symlink inside an otherwise plausible directory was not resolved against an attachment boundary before delivery.

After this PR

  • MEDIA[:] paths are validated with BasePlatformAdapter.validate_media_delivery_path() before they are returned for attachment delivery.
  • The path must:
    • be absolute after ~ expansion;
    • exist;
    • be a regular file;
    • resolve successfully;
    • resolve under a Hermes-managed media cache root or an explicit operator allowlist root.
  • Symlinks are resolved before containment checks, blocking safe-looking links that point outside allowed roots.
  • Operators can opt in additional attachment roots with HERMES_MEDIA_ALLOW_DIRS when they intentionally want a local directory to be deliverable.

Why this matters

Gateway MEDIA[:] tags are a protocol boundary between model text and host file I/O. If the model can be induced to emit a local path, the gateway may upload that file to the user's messaging platform. That creates a high-impact confidentiality risk for any secrets, transcripts, logs, screenshots, exports, or other host files with supported extensions.

The safe default is that only files created or cached for Hermes media delivery should be attachable. Arbitrary local paths should remain text unless the operator explicitly marks a directory as deliverable.

Verified live behavior

During validation on a live Telegram-backed Hermes gateway, a response that contained the media-delivery prefix followed by /etc/passwd caused the gateway to upload the local /etc/passwd file into Telegram. This was not just visible text in the message body; the platform delivery layer treated the assistant output as an attachment instruction and sent the referenced host file.

This happened because the gateway parsed assistant text after generation, extracted the local path from the directive-like output, and handed that path to the Telegram adapter for native file delivery without first enforcing a host-file containment boundary. Markdown/code-fence formatting did not provide a security boundary because parsing happens on the outgoing response text rather than on a trusted structured tool result.

/etc/passwd is used here only as a reproducible low-risk system-account metadata file. It should still never be deliverable from model-controlled text, and the same behavior would be more serious for secrets, logs, screenshots, transcripts, exported documents, or other readable host files.

How this differs from #6084

This PR is in the same public security family as #6084, but it fixes a stricter boundary.

  • fix(gateway): validate MEDIA tag paths to block arbitrary local file … #6084 validates that MEDIA[:] values are local absolute paths with known media extensions.
  • This PR validates the delivery boundary: the resolved file must live under Hermes-managed media cache roots, or under an explicit operator allowlist root.
  • Extension validation alone still allows exfiltration of arbitrary host files with allowed-looking extensions, such as local screenshots, exported PDFs, images, archives, audio/video files, or renamed secret material.
  • This PR also resolves symlinks before the containment check so an allowed cache path cannot point outside the deliverable area.

Both approaches reduce risk, but containment is the security boundary that prevents arbitrary host file reads/uploads through model-emitted attachment directives.

Attack flow

1. Attacker influences a conversation or injected context seen by the assistant.
2. Assistant emits a response containing a local attachment directive, for example:
   MEDIA[:]/some/host/private-export.pdf
3. The gateway extracts the MEDIA tag from assistant text.
4. The platform adapter treats the path as a native attachment.
5. The local file is uploaded to the messaging platform if the gateway process can read it.

Affected code

Area File Notes
Gateway attachment extraction gateway/platforms/base.py BasePlatformAdapter.extract_media() parsed model-emitted MEDIA[:] tags into local paths
Gateway send path gateway/platforms/base.py Extracted media files are later delivered via native platform APIs
Regression coverage tests/gateway/test_platform_base.py Existing extraction tests now use real safe-root files and cover rejection cases

Root cause

  • Assistant/model text was treated as an instruction to read and upload local files.
  • The parser validated syntax, not authorization to deliver the referenced file.
  • No containment check tied deliverable MEDIA[:] files to Hermes-generated/cache-managed outputs.
  • Symlink resolution was not part of the trust-boundary decision.

CVSS assessment

  • Issue: Gateway MEDIA[:] local-file exfiltration via model-controlled attachment directives
  • CVSS v3.1: 7.7 High
  • Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:N

Rationale: an attacker generally needs a way to influence assistant output or conversation context, and the gateway user/session must receive the crafted response. If successful, the impact crosses from model text into host file disclosure through a messaging platform attachment, with high confidentiality impact and no direct integrity or availability impact claimed here.

Safe reproduction steps

A safe regression shape is included in the tests:

  1. Create a file outside the configured safe media root with a deliverable-looking extension, such as outside-secret.png.
  2. Emit MEDIA[:]<outside-file> in gateway response text.
  3. Confirm the path is not extracted into the attachment list and the text remains plain content.
  4. Create a symlink inside the safe root that points to the outside file.
  5. Emit MEDIA[:]<safe-root-symlink>.
  6. Confirm the symlink is rejected because its resolved target is outside the allowed root.

Expected vulnerable behavior

On vulnerable code, extract_media() returns expanded absolute paths directly from the assistant response. A MEDIA[:] directive that names a readable local file with a supported extension can be passed to the platform delivery path without proving the file came from a Hermes media cache or operator-approved root.

Changes in this PR

  • Adds MEDIA_DELIVERY_SAFE_ROOTS for Hermes-managed media/document/screenshot cache directories.
  • Adds HERMES_MEDIA_ALLOW_DIRS for explicit operator opt-in roots.
  • Adds validate_media_delivery_path() to require absolute, existing, regular files.
  • Resolves candidate paths before checking containment to block symlink escape.
  • Updates extract_media() to return only validated media paths.
  • Updates media extraction tests to use real temporary files under allowed roots.
  • Adds regressions for arbitrary existing media-file rejection, unsafe extensionless path rejection, code-block directive rejection, symlink escape rejection, and explicit allowlist roots.

Files changed

File Change
gateway/platforms/base.py Adds media delivery safe roots, optional allowlist roots, path validation, and extraction enforcement
tests/gateway/test_platform_base.py Updates extraction tests and adds security regressions

Maintainer impact

  • Normal Hermes-generated attachments continue to work from image/audio/video/document/screenshot cache directories.
  • Invalid or unsafe MEDIA[:] tags are left as plain text instead of being silently converted into attachments.
  • Operators with custom local media workflows can set HERMES_MEDIA_ALLOW_DIRS to opt in specific directories.
  • This PR does not remove MEDIA[:] support; it narrows the local-file delivery boundary.

Fix rationale

The gateway should authorize file delivery based on where the file came from, not on whether model text contains a plausible file extension. Containing attachment delivery to Hermes media caches preserves intended generated-media behavior while preventing prompt-influenced arbitrary host file disclosure.

Type of change

  • Security fix
  • Regression tests
  • Documentation-only change
  • Breaking API change

Test plan

Commands run locally:

python -m pytest tests/gateway/test_platform_base.py -q
python -m ruff check --ignore E402 gateway/platforms/base.py tests/gateway/test_platform_base.py
git diff --check
python -m ruff format --check tests/gateway/test_platform_base.py

Results:

81 passed in 0.81s
All checks passed!
1 file already formatted

Note: gateway/platforms/base.py currently has pre-existing E402 import-order findings in this repository layout, so the targeted ruff command ignores E402 and checks the security-relevant changes plus the updated tests.

Disclosure notes

This PR is intentionally bounded to the gateway MEDIA[:] directive local-file delivery boundary. It does not claim to address every possible prompt-injection path, every gateway attachment path, or every local file reference feature. It specifically prevents model-emitted MEDIA[:] attachment directives from reading and sending arbitrary host files unless those files are under Hermes-managed media caches or explicitly operator-allowlisted directories.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/gateway Gateway runner, session dispatch, delivery labels Apr 27, 2026
@Hinotoi-agent Hinotoi-agent force-pushed the fix/gateway-media-directive-containment branch from a6459a7 to 1303ac9 Compare April 28, 2026 11:54
@Hinotoi-agent

Copy link
Copy Markdown
Contributor Author

Rebased onto current main to clear the merge conflict/DIRTY state and pushed the cleaned branch.

Current head: 1303ac95

The only rebase conflict was in tests/gateway/test_platform_base.py; I preserved the newer proxy tests from main and kept the MEDIA directive containment regressions from this PR.

Validation after rebase:

  • python -m pytest tests/gateway/test_platform_base.py -q — 83 passed, 2 skipped
  • python -m ruff check --ignore E402 gateway/platforms/base.py tests/gateway/test_platform_base.py — passed
  • python -m ruff format --check tests/gateway/test_platform_base.py — passed
  • git diff --check — passed
  • added-line secret scan — no findings

@Hinotoi-agent

Copy link
Copy Markdown
Contributor Author

Rebased onto upstream main and resolved the gateway MEDIA directive test conflict. New head: 601689d03a63a63797f531d957afa7be500491b7. Local validation: /Users/lennon/.hermes/hermes-agent/venv/bin/python -m pytest tests/gateway/test_platform_base.py -q → 90 passed, 2 skipped.

@Hinotoi-agent

Copy link
Copy Markdown
Contributor Author

Pushed e0b67faaf to address the failing test job after the MEDIA containment rebase.

Root cause: extract_media() had been made to reject unsafe paths immediately, but existing delivery paths/tests expect extraction to parse and strip syntactic MEDIA: tags before routing. The security boundary now stays at delivery time:

  • extract_media() parses/strips MEDIA: tags as before.
  • validate_media_delivery_path() still enforces existing regular files under Hermes media cache roots or explicit operator allow roots.
  • _process_message_background() validates each parsed MEDIA path before sending, and skips unsafe paths.
  • Regression tests now cover extraction vs delivery validation separately.

Local validation:

python -m pytest \
  tests/gateway/test_platform_base.py::TestExtractMedia \
  tests/gateway/test_send_image_file.py::TestExtractMediaImages \
  tests/gateway/test_signal.py::TestSignalMediaExtraction \
  tests/gateway/test_tts_media_routing.py \
  tests/cron/test_scheduler.py::TestDeliverResultWrapping \
  -q
# 39 passed

python -m ruff check tests/gateway/test_platform_base.py tests/gateway/test_tts_media_routing.py
python -m ruff format --check tests/gateway/test_platform_base.py tests/gateway/test_tts_media_routing.py
python -m compileall -q gateway/platforms/base.py tests/gateway/test_platform_base.py tests/gateway/test_tts_media_routing.py
git diff --check

Note: python -m ruff check gateway/platforms/base.py still reports pre-existing E402 import-order findings around the repo's intentional path setup/import block; I did not include unrelated cleanup in this security PR.

@Hinotoi-agent Hinotoi-agent force-pushed the fix/gateway-media-directive-containment branch 2 times, most recently from 6ebe65a to aa6e0d2 Compare May 4, 2026 00:27
@Hinotoi-agent

Hinotoi-agent commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Pushed follow-up a687293ab after aa6e0d222 to address the failing test job and rebased the branch onto current main.

What changed:

  • Rebased fix/gateway-media-directive-containment from 6ebe65a58 onto upstream main.
  • Restored the explicit You are a Kanban worker marker in KANBAN_GUIDANCE, matching the worker-prompt regression test while keeping the newer Kanban task-execution protocol wording.
  • Stabilized the hermes update --yes autostash regression test by patching the exact cmd_update globals dict used by the imported function, so full-suite runs that reload/replace module symbols still verify prompt_user=False instead of accidentally invoking the real restore helper.

Local validation:

python -m pytest \
  tests/gateway/test_platform_base.py \
  tests/hermes_cli/test_update_yes_flag.py::TestUpdateYesStashRestore::test_yes_restores_stash_without_prompting \
  tests/tools/test_kanban_tools.py::test_kanban_guidance_in_worker_prompt \
  -q
# 92 passed, 2 skipped

python -m ruff check agent/prompt_builder.py tests/hermes_cli/test_update_yes_flag.py
# passed

git diff --check
# passed

GitHub Actions after the push are now passing, including the previously failing test job: https://github.com/NousResearch/hermes-agent/actions/runs/25295406322/job/74153307659

@Hinotoi-agent Hinotoi-agent force-pushed the fix/gateway-media-directive-containment branch 2 times, most recently from 6a34df4 to 1812538 Compare May 9, 2026 13:35
@Hinotoi-agent Hinotoi-agent force-pushed the fix/gateway-media-directive-containment branch from e210e63 to 498d0cc Compare May 13, 2026 03:39
@teknium1

Copy link
Copy Markdown
Contributor

Superseded by PR #30432 (merged 41d2c75, credit @egilewski). The merged fix covers all MEDIA delivery sites (not just extract_media in one adapter), resolves symlinks before containment checks, and ships with regression coverage across cron, gateway, weixin, send_message, and yuanbao. Thanks for catching this class early — the four parallel PRs on this issue were what told us this was a real defense gap worth landing.

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 type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants