Skip to content

fix(send_message): add per-site defense-in-depth media path validation (#34270)#34350

Open
Bartok9 wants to merge 1 commit into
NousResearch:mainfrom
Bartok9:fix/34270-send-message-defense-in-depth
Open

fix(send_message): add per-site defense-in-depth media path validation (#34270)#34350
Bartok9 wants to merge 1 commit into
NousResearch:mainfrom
Bartok9:fix/34270-send-message-defense-in-depth

Conversation

@Bartok9

@Bartok9 Bartok9 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Closes #34270

Context

send_message_tool already filters via BasePlatformAdapter.filter_media_delivery_paths() before dispatching to platform-specific senders, so the original reproduction (MEDIA:/etc/hosts) is no longer reproducible end-to-end on main. The upstream filter catches /etc/, /var/log/, etc.

However the per-platform send sites still open and upload the file directly with no allowlist check of their own. That trust boundary is implicit and local to whoever happens to call those helpers next.

Why this matters

A future caller that constructs media_files by another path — restoring from session storage, a new admin tool, a plugin hook, a session-resume edge case — could bypass the upstream filter and exfiltrate files. The fix moves the trust boundary inside the actual file-open operations.

Changes

File Function Change
tools/send_message_tool.py _send_telegram validate each path before open/upload
tools/send_message_tool.py _send_signal validate each path before attaching

Both import validate_media_delivery_path lazily and fail-closed on import error (refuse rather than upload unvalidated). On reject: warning logged, surfaced in result['warnings'], file skipped cleanly. Text portion still delivers.

Tests (3 new)

  • test_send_telegram_rejects_path_outside_safe_roots — calls _send_telegram() directly with unsafe path (the bypass pattern), confirms file NOT delivered + warning surfaces.
  • test_send_telegram_accepts_path_inside_safe_roots — happy path: safe file delivers cleanly.
  • test_send_signal_rejects_path_outside_safe_roots — Signal mirror test: unsafe path excluded from bridge payload + warning logged.
$ python -m pytest tests/tools/test_send_message_tool.py
=== 124 passed in 2.83s ===

🎻 Co-authored-by: Cursor cursoragent@cursor.com

NousResearch#34270)

The send_message_tool already filters media paths through
BasePlatformAdapter.filter_media_delivery_paths() before dispatch to
_send_telegram() / _send_signal(), so the upstream filter currently
catches /etc/passwd, /var/log/*, etc. The original reproduction in
NousResearch#34270 ("send /etc/hosts") is no longer reproducible end-to-end.

However the per-platform send sites still open and upload the file
directly with no allowlist check of their own. That trust boundary
is implicit and local to whoever happens to call those helpers next.
A future caller that constructs media_files by another path \u2014 e.g.
restoring from session storage, a new admin tool, a plugin hook \u2014
could bypass the upstream filter and exfiltrate files via attachment.

Fix: add validate_media_delivery_path() at the I/O sites themselves,
mirroring the upstream filter behavior. This is pure defense in depth
\u2014 the upstream filter still does the heavy lifting, but the
trust boundary is now local to the actual file-open operation.

Coverage:

- tools/send_message_tool.py::_send_telegram \u2014 validates each path
  against the allowlist before os.path.exists / open(). On reject:
  logs a warning, appends to result['warnings'], skips the file
  cleanly.

- tools/send_message_tool.py::_send_signal \u2014 same validation
  applied before adding to attachment_paths. The Signal bridge never
  sees the unsafe path; existing path-not-found semantics
  preserved.

Both helpers import validate_media_delivery_path lazily and
fail-closed on import error (refuse to upload rather than upload
unvalidated).

Tests (3 new):

- TestSendTelegramRevalidatesMediaPaths::
  test_send_telegram_rejects_path_outside_safe_roots
    \u2014 Calls _send_telegram() DIRECTLY with an unsafe path (the
    pattern a future caller might use accidentally), confirms file
    is NOT delivered, warning surfaces in result, text portion still
    delivers.

- TestSendTelegramRevalidatesMediaPaths::
  test_send_telegram_accepts_path_inside_safe_roots
    \u2014 Happy path: file inside MEDIA_DELIVERY_SAFE_ROOTS delivers
    cleanly, no warnings.

- TestSendSignalRevalidatesMediaPaths::
  test_send_signal_rejects_path_outside_safe_roots
    \u2014 Mirror test for Signal. Unsafe path is excluded from the
    Signal bridge attachments payload; warning is logged.

All 124 tests in test_send_message_tool.py pass.

Refs: NousResearch#34270
Closes: NousResearch#34270

Co-authored-by: Cursor <cursoragent@cursor.com>
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets platform/telegram Telegram bot adapter platform/signal Signal CLI adapter labels May 29, 2026
@liuhao1024

Copy link
Copy Markdown
Contributor

I found one issue worth fixing before merge.

tools/send_message_tool.py_send_signal fails open when the validator import fails, defeating the defense-in-depth guard.

In _send_telegram (lines ~947–960), the import is wrapped in a try/except that sets _safe_path = None on failure, causing the media file to be skipped (fail-closed):

try:
    from gateway.platforms.base import validate_media_delivery_path
    _safe_path = validate_media_delivery_path(media_path)
except Exception:
    _safe_path = None  # fail-closed
if not _safe_path:
    # skip — file not delivered
    continue

In _send_signal (lines ~1142–1155), the same import failure sets validate_media_delivery_path = None, but the guard if validate_media_delivery_path is not None: then skips validation entirely, allowing all media paths through unvalidated (fail-open):

try:
    from gateway.platforms.base import validate_media_delivery_path
except Exception:
    validate_media_delivery_path = None  # fail-open!
for media_path, _is_voice in valid_media:
    if validate_media_delivery_path is not None:
        # validation only runs if import succeeded

Why it matters: The PR's stated purpose is defense-in-depth — a future caller that skips the upstream filter shouldn't be able to exfiltrate files through these send sites. But if the gateway.platforms.base module fails to import (e.g., circular import, missing dependency in a minimal install), _send_signal silently delivers any path without checking. The Telegram path correctly fails closed; the Signal path should match.

Suggested fix: Move the import outside the loop (it already is) but treat import failure the same way as _send_telegram — skip all media files if the validator is unavailable:

try:
    from gateway.platforms.base import validate_media_delivery_path
except Exception:
    validate_media_delivery_path = None

for media_path, _is_voice in valid_media:
    if validate_media_delivery_path is None:
        logger.warning("Signal: media path validator unavailable, skipping %s", media_path)
        continue
    safe_path = validate_media_delivery_path(media_path)
    if not safe_path:
        logger.warning("Signal: skipping unsafe media path outside allowed roots: %s", media_path)
        continue
    media_path = safe_path
    # ... rest of existing logic

This makes both send sites fail-closed consistently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/signal Signal CLI adapter platform/telegram Telegram bot adapter type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: send_message tool bypasses validate_media_delivery_path security check

3 participants