fix(send_message): add per-site defense-in-depth media path validation (#34270)#34350
fix(send_message): add per-site defense-in-depth media path validation (#34270)#34350Bartok9 wants to merge 1 commit into
Conversation
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>
|
I found one issue worth fixing before merge.
In 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
continueIn 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 succeededWhy 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 Suggested fix: Move the import outside the loop (it already is) but treat import failure the same way as 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 logicThis makes both send sites fail-closed consistently. |
Closes #34270
Context
send_message_toolalready filters viaBasePlatformAdapter.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_filesby 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
tools/send_message_tool.py_send_telegramtools/send_message_tool.py_send_signalBoth import
validate_media_delivery_pathlazily and fail-closed on import error (refuse rather than upload unvalidated). On reject: warning logged, surfaced inresult['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