fix(channels): stop email replies from falling back to reply_to as an… - #1644
Closed
Preciousuche wants to merge 1 commit into
Closed
Preciousuche wants to merge 1 commit into
Preciousuche wants to merge 1 commit into
Conversation
… address EmailChannel._resolve_target fell back to treating reply_to as a raw mailbox (via is_email_address) whenever the thread it named was unknown. reply_to always names a thread -- the inbound Message-ID -- never a mailbox by itself, but a Message-ID has the same local@domain shape as a real address, and its domain is chosen by whoever sent the original mail. self._threads is an in-memory, LRU-bounded cache that is empty after any process restart, so once a thread aged out, the fallback silently redirected the reply to whatever domain the original sender's Message-ID happened to carry -- a mailbox they control. A smarter is_email_address cannot fix this: a Message-ID is genuinely indistinguishable from an address by shape. Removed the fallback entirely; _resolve_target now raises when the thread is unknown and no explicit metadata["to"]/["recipient"] is present, converting a silent misdelivery into a loud, actionable failure. Checked every real producer that relies on reply_to alone for email (scheduler/heartbeat delivery, artifact_delivery.py's send_file calls) -- they all pass an existing channel_id, i.e. a thread the cache is expected to still know about, and none has a legitimate case for reply_to ever meaning a fresh, unseen mailbox. The message tool already supplies metadata["recipient"] unconditionally, so it was never relying on the fallback either. Two existing tests encoded the old (vulnerable) contract as intended behavior and needed updating to the corrected one. Fixes use-agent-os#1570
Contributor
|
Thanks for the PR. #1570 was fixed by #1847, which also updates the scheduler/heartbeat producers. This branch removes the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EmailChannel._resolve_target fell back to treating reply_to as a raw mailbox (via is_email_address) whenever the thread it named was unknown
reply_to always names a thread — the inbound Message-ID — never a mailbox by itself, but a Message-ID has the same local@domain shape as a real address, and its domain is chosen by whoever sent the original mail
self._threads is an in-memory, LRU-bounded cache that is empty after any process restart, so once a thread aged out, the fallback silently redirected the reply to whatever domain the original sender's Message-ID happened to carry — a mailbox they control
A smarter is_email_address cannot fix this: a Message-ID is genuinely indistinguishable from an address by shape
Removed the fallback entirely; _resolve_target now raises when the thread is unknown and no explicit metadata["to"]/["recipient"] is present, converting a silent misdelivery into a loud, actionable failure
Checked every real producer that relies on reply_to alone for email (scheduler/heartbeat delivery, artifact_delivery.py's send_file calls) — they all pass an existing channel_id, i.e. a thread the cache is expected to still know about, and none has a legitimate case for reply_to ever meaning a fresh, unseen mailbox. The message tool already supplies metadata["recipient"] unconditionally, so it was never relying on the fallback either
Two existing tests encoded the old (vulnerable) contract as intended behavior — one literally docstringed "scheduler and heartbeat delivery pass the address as reply_to alone" — and needed updating to the corrected one
Fixes #1570
Test plan
Added a direct security regression test reproducing the issue's exact steps: receive an inbound email whose Message-ID carries an attacker-controlled domain, evict the thread cache (simulating LRU eviction or a restart), attempt the reply, confirm it's refused rather than misdelivered
Added a general "unknown reply_to that looks like an address" test
Updated the two existing tests that had encoded the vulnerable fallback as expected behavior, to the corrected precedence chain (to > recipient > known thread, with a clear error when none apply)
uv run pytest tests/test_channels/test_email_channel.py -q — 85 passed
uv run pytest tests/test_channels tests/test_gateway -q — 1948 passed, 1 skipped (full suite, unrelated to this change)
uv run ruff check / uv run mypy clean on changed files