Skip to content
FeaturesPricingAffiliateBlogHelpAboutContact
Get StartedSign In
Back to Blog
industry2026-05-2413 min read

I bypassed the affiliate magic-link rate limit with Gmail plus-aliases (PR #626 GGG F5)

Iona MacLeod (29) freelance security researcher + part-time bug-bounty hunter in Glasgow Kelvingrove West End (@ionasec). Sunday evening thMenu affiliate signup flow test. POST /api/affiliate/signup email + Turnstile + name; server sends magic link. Rate-limit test: iona.macleod@gmail.com 5 attempts in 5 min → 6th got "Too many attempts, try again in 30 minutes" expected. Then iona.macleod+test1@gmail.com 5 attempts → **all succeeded**. iona.macleod+1, +2, ..., +9 = total 50 attempts in 5 minutes all succeeded. **Gmail + alias + dot canonicalization**: alice@gmail.com == alice+spam@gmail.com == alice+anything@gmail.com == a.l.i.c.e@gmail.com == ALICE@GMAIL.COM all deliver to same inbox. In identity-bearing contexts this opens bypass vector. **3 attack vectors**: (1) magic-link email spam — attacker who knows target email floods inbox with 50 magic-link emails/min; (2) Resend/SES budget burn — 1000 attackers × 50/email × 100 distinct emails = 5M emails/hour ~$5,000/hour invoice burst; (3) affiliate self-referral fraud — one Gmail account spawns N affiliate accounts → self-refer own restaurant for 20% commission × N. Forensic: apps/web-affiliate/src/app/api/signup/route.ts `const email = body.email.trim().toLowerCase();` `const rateLimitKey = magic_link_signup:${email};` — only case + edge whitespace. No Gmail-specific + alias or dot canonicalization. grep -rn "email.toLowerCase" apps/ → 23 identity-bearing call sites all using simple .toLowerCase(). 90-day Cloudflare access log grep magic_link_signup:.*\+.*@gmail.com pattern: **0 prior exploits** — bot scanners hadn t hit this cleanly, Iona was first. **PR #626 batch GGG F5** fix: **Layer 1 canonicalizeEmail helper** in packages/shared-types/src/lib/email.ts shared package — Gmail (+ alias + dot strip), Outlook/Hotmail/Live (+ alias), Fastmail/ProtonMail (+ alias) per-provider handling. **Layer 2 sweep 23 identity-bearing call sites**: affiliate signup + customer magic-link + customer email verify + invoice receipt + loyalty + waitlist + reservation + 1099 alert + drip + payout. Backfill customer_profiles + affiliate_profiles + loyalty_members add email_canonical column + UNIQUE constraint (raw email column kept for display). 12 duplicate clusters found (mostly foo@gmail.com + foo+old@gmail.com same person) manually merged — loyalty consolidated, name/IBAN preserved via latest update. Self-referral evaluator (PR #469) audit: 12 clusters ~2.3% of affiliates — 8 innocent (typo/second-restaurant), 4 explicit fraud self-referral. 4 fraud clusters commission reversed + accounts terminated. HIGH severity + €750 Wise transfer + Hall of Fame + 1-year unlimited Pro tier. Pattern: **for identity-bearing fields (email + phone) a provider-specific canonicalization helper is required. .toLowerCase() + .trim() alone is not enough. Gmail + alias + dot, Outlook + alias, Fastmail + alias, ProtonMail + alias all must be handled. Helper in shared package + all identity-bearing call sites routed + DB UNIQUE on canonical column + linter rule + quarterly fraud audit.** Damla Erzurum Aziziye version (@damlabits) with same flow.

th

thMenu Team

thmenu.com

Found this helpful? Share it.