Build1 publisher2 min readPublished
One unthrottled signup endpoint gave a bot six API keys in six hours
A developer's signup route issued six API keys overnight to a bot using dot-mutated Gmail addresses. Stripped of dots they were six strangers' inboxes, so canonicalizing emails would not have stopped it.
The Engineer · Build desk

What happened
- The uniqueness check compared lower(email), so six dotted strings produced six rows and six API keys.
- The signup endpoint had no issuance budget, while the request-key endpoint beside it capped each address at five keys a day.
- Email verification is off by default in that deployment, so signup returned a working key in the response with no inbox needed.
- The fix was about four lines calling the existing throttle function from signup and returning HTTP 429 when it refuses.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint While verification stays off by default, a unique email column cannot cap key issuance, since a bot never has to open the inbox; the cap has to sit on every route that mints a key.
- exposure The sending domain carries the risk: verification mail to six strangers invites spam reports, and the author warns a few on a young domain can silently stop delivery to real users.
- decision Canonical-email rules have to be set per provider, because folding dots everywhere would turn away real Outlook users whose addresses differ by one dot, and those users leave without complaining.
- cost Adding a UNIQUE canonical index to a live database can fail the migration, and each failure is an existing pair of accounts sharing an inbox that has to be sorted out before it lands.
Gmail ignores dots in the local part of an address, so j.o.h.n@gmail.com and john@gmail.com reach the same inbox [2]. The behaviour is documented [2]. Lowercasing does nothing about it [3]. One person could have farmed keys from a single Gmail inbox, and that hole was open in this deployment. The six overnight signups did not need it [1].
Run the author's canonical_email function over the six addresses and it returns six distinct values, one per stripped full name [1]. A unique index on that column would have accepted all six accounts [1]. The author concluded the list was harvested addresses of real people with dots inserted [4]. In the post's words, the mutation was "a generic evasion habit, applied to a list, by something that signs up for things" [17]. With verification off, the bot never had to open any of those inboxes [9].
The budget on POST /v1/auth/request-key sits under a comment saying it bounds key-farming [7]. The comment was accurate about the endpoint it sat above. The fix calls the existing _request_key_throttle from signup and raises a 429 when it refuses [10]. Its argument is client_ip, so the budget counts requests from a network address and never looks at the email typed [10]. The author's diagnosis: "You write the dangerous-looking endpoint carefully because it looks dangerous, and the ordinary one stays ordinary." [11]
The key was worth farming. A day earlier the team had opened a package-security lookup to keyless callers at 20 requests an hour per address and five packages per request [6]. That comes to 100 package lookups an hour per address [2]. "An API key skips all of it," the author wrote [16].
Canonicalization still belongs in the schema, and the post does it carefully. Dots stop mattering only at Gmail. At Outlook, first.last@outlook.com and firstlast@outlook.com are two separate mailboxes [12]. The author's function strips plus-tags for 11 listed provider domains and strips dots for two, gmail.com and googlemail.com, after mapping googlemail.com to gmail.com [13][3]. Its output goes in a separate column with a unique index, while mail still goes to the address as typed [14].
I'd copy the index decision as written. Declaring it UNIQUE lets the migration fail when two existing accounts share an inbox, and a WHERE NOT EXISTS would have hidden that pair [15].
What to watch
- Whether the six signups came from one client IP or many; the fix counts its budget by client IP, and the post does not say.
- Whether the deployment turns email verification on by default, at which point a bot would need a readable inbox to get a key.
- Spam complaints or delivery failures on the young sending domain after verification mail reached six strangers.