feat(email): key limits by mailbox and account - #41
Open
johnhooks wants to merge 1 commit into
Open
Conversation
Per-recipient limits counted the address as typed, so alice@gmail.com, alice+1@gmail.com and a.l.i.c.e@gmail.com each earned a separate budget against one inbox: enough to type past the cap. Anything whose effect is mail now counts the mailbox an address lands in. The address sent to WorkOS is untouched; the reduced form is a counter key, never a destination. Rewrites apply per provider rather than everywhere. Merging two real mailboxes lets either starve the other, and business+brian@corp.com is routinely its own mailbox with its own owner; the opposite error costs only a counter per tag, still capped per IP. RFC 5233 calls the encoding site-specific, and Yahoo, iCloud and Proton each disagree with the convention, so both lists stay fixed and conservative. Guessing goes the other way: WorkOS keys accounts on the exact string, so alice@ and alice+work@ can both exist with their own passwords, and merging them would let an attacker on one lock out the other. Password and code attempts count the address as typed, so the two subjects differ within one request. Magic verify also rejects malformed addresses now, instead of any non-empty string.
johnhooks
force-pushed
the
feature/email-canonicalizer
branch
from
August 4, 2026 22:19
70b7987 to
aa04862
Compare
johnhooks
commented
Aug 4, 2026
Comment on lines
+102
to
+107
| if ( in_array( $domain, self::PLUS_TAG_DOMAINS, true ) ) { | ||
| $plus = strpos( $local, '+' ); | ||
| if ( false !== $plus ) { | ||
| $local = substr( $local, 0, $plus ); | ||
| } | ||
| } |
Collaborator
Author
There was a problem hiding this comment.
You were right @redscar thanks for calling it out. #38 (comment)
There are still some edge cases, though they are not inadvertently modified, but come through without modification, possibly targeting multiple email addresses but not on the common platforms.
johnhooks
marked this pull request as ready for review
August 4, 2026 22:22
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.
Branch:
feature/email-canonicalizer→feature/client-ip. Stacked on #40, which is itself stacked on #39; review those first.Per-recipient limits counted the address as typed, so one inbox was handed a fresh allowance for every variant a caller could think of.
alice@gmail.com,alice+1@gmail.com, anda.l.i.c.e@gmail.comare three strings and one mailbox, and each of them got its own budget, which is enough to undo the caps in #39 by typing a different tag each time. This branch counts against the mailbox an address actually lands in, and separately fixes what that broke for accounts.Changes
*_recipientbuckets plus the per-request limits on the routes that send (magic_send_email,pw_reset_email,signup_email). The address handed to WorkOS is untouched; the reduced form is a counter key and never a destination.password_email,magic_verify_email) count the address as typed. WorkOS keys accounts on the exact string, soalice@andalice+work@can both be registered with their own passwords, and counting them together would let an attacker hammering one lock out the other. The two subjects now differ inside a single request on purpose.+as a tag gets a counter per tag, which is the milder failure and still bounded by the per-IP limits.Why the provider list, rather than stripping everywhere
Guessing wrong is not symmetric. Collapsing two real mailboxes into one counter lets either exhaust the other's allowance, and on corporate or self-hosted mail
business+brian@corp.comis routinely its own mailbox with its own owner, so a blanket rule turns a spam control into a way to lock a colleague out. Splitting one mailbox across counters is the milder failure, because the per-IP limits from #39 still cap what any single caller can cause.The convention is also less universal than it looks. RFC 5233 states that the encoding of detailed addresses is "site and/or implementation specific" and declines to standardise a separator, RFC 5321 leaves local-part semantics to the destination host, and among large providers Yahoo and iCloud offer disposable addresses rather than tags while Proton uses a different separator.
Behavior changes to note
Not included: Proton's
-separator. Treating-as a tag marker needs its own list and better evidence, sincejohn-smith@corp.comis an ordinary mailbox name. Fastmail's subdomain form (tag@user.fastmail.com) is likewise unhandled.Details in the "Which mailbox" section of
docs/rate-limiting.md, including which buckets must stay canonical if anyone extends the exact-address keying.