Skip to content

fix(email): quoted display-name brackets can override the real sender address#151

Open
ygd58 wants to merge 1 commit into
runta-dev:mainfrom
ygd58:fix/email-quoted-display-name-address-spoof
Open

fix(email): quoted display-name brackets can override the real sender address#151
ygd58 wants to merge 1 commit into
runta-dev:mainfrom
ygd58:fix/email-quoted-display-name-address-spoof

Conversation

@ygd58

@ygd58 ygd58 commented Jul 21, 2026

Copy link
Copy Markdown

Bug: extract_sender_address() in src/email.rs picks the address from the last <...> pair anywhere in the From header (rfind('<')/rfind('>')). If a quoted display-name that happens to follow the real address contains its own bracketed text, e.g.:

<alice@trusted.org> "Bob <fake@evil.com>"

...the function returns the decoy fake@evil.com instead of the actual sender alice@trusted.org, because the last-bracket search has no concept of quoting. Since EmailPolicy::sender_visible() drives allowlist/denylist filtering directly off this return value, a crafted display-name can cause a message to be mis-classified relative to who actually sent it.

Fix: replaced the naive last-bracket search with extract_bracketed_address() — a single left-to-right scan that tracks whether it's inside a double-quoted quoted-string (respecting backslash-escaped quotes, e.g. \") and only treats unquoted </> as address delimiters. This resolves the real address correctly regardless of whether a bracketed decoy appears before or after it.

Tests added (all passing, cargo test email:: — 15 passed / 0 failed):

  • decoy bracket after the real address (the bug case above)
  • decoy bracket before the real address (previously-working direction, kept as a regression guard)
  • an escaped-quote edge case inside the display-name, to make sure \" doesn't prematurely flip the in-quotes state and expose a decoy bracket

Verification note: I drafted this with an AI pair-programming assistant (Claude) working from a sandbox without a matching Rust toolchain for this crate's edition, so the function was first verified standalone with plain rustc against all 5 cases, and then I pulled the branch and ran the actual crate's test suite locally (cargo test email::) before opening this PR — output above.

No behavior change for the common case (single unquoted <addr> pair) or for headers with no brackets at all (unchanged fallback path).

…sender address

extract_sender_address() picked the address from the *last* <...> pair
found anywhere in the From header. If a quoted display-name that follows
the real address happens to contain its own bracketed text, e.g.:

    <alice@trusted.org> "Bob <fake@evil.com>"

the function returned the decoy fake@evil.com instead of the real sender
alice@trusted.org, because rfind('<')/rfind('>') don't know about quoting.
Since EmailPolicy::sender_visible() drives allowlist/denylist filtering
directly off this value, a crafted display-name could cause a message to
be mis-classified relative to the actual sender.

Replaced the naive last-bracket search with extract_bracketed_address(),
a single left-to-right scan that tracks whether it's inside a double-quoted
quoted-string (respecting backslash-escaped quotes) and only treats
unquoted '<'/'>' as address delimiters. This correctly resolves the real
address regardless of whether a bracketed decoy appears before or after it
in the header.

Added regression tests for: decoy after the real address, decoy before it
(previously-working direction, kept as a guard), and an escaped-quote
edge case inside the display-name.

Note on verification: this sandbox only has rustc 1.75 available via apt,
and the crate needs edition 2024 (~1.85+), so I couldn't run the full
'cargo test' suite here. I extracted the new function verbatim into a
standalone file and compiled+ran it with rustc directly against all 5
test cases (3 old passing behaviors + 2 new ones) -- all passed. Please
run 'cargo test --lib email' locally to confirm before merging.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant