Email bombing on WhiteMarket: racing the verification-resend limit
The resend-verification endpoint was rate limited, and it even returned 429 under load. But the limit was not atomic, so a parallel burst slipped a stack of emails through before the limiter caught up.
WhiteMarket (white.market) is a marketplace for gaming items. In profile settings, an unverified account can ask the site to resend its verification email. That action is rate limited, so a normal user cannot spam it. The limit, though, was not enforced atomically: firing many copies of the request at the same instant let a burst through at once, and each one sends a real email. The result is a mailbox flooded on demand. Reported to WhiteMarket on HackenProof, rated Low, and resolved.
The idea in one line
A rate limit normally reads a counter to see how many requests you have made, and if you are under the cap it lets the request through and adds one. Those are two steps. If requests arrive one at a time, the counter is updated before the next is checked and the cap holds. If a pile of requests arrive at the exact same moment, they can all read the counter before any of them has incremented it, so they all pass. That is a race condition, and it is beaten by sending in parallel rather than by sending more.
The limiter here is not absent. Under the parallel load it does eventually fire and
return 429 Too Many Requests with a Retry-After. The point
is that a meaningful number of requests already succeeded in the moment before it
engaged. A limiter that reacts a beat late is still a limiter you can outrun.
The attack
-
Find the resend action
Log in and open profile settings at
/me?tab=profile-settings. The account details panel has a Resend verification e-mail link.
The resend action in profile settings. The email address is blurred. -
Capture the request
Clicking resend sends a GraphQL call,
POST /graphql/apitoapi.white.market. Send it to Burp Suite. -
Group it and fire in parallel
Duplicate the identical request into a group of roughly a hundred tabs and use Burp's send group in parallel so they all leave together and land in the gap between the limiter's check and its update.
A group of ~100 identical requests sent in parallel. The limiter does engage and return 429withRetry-After: 8, but only after a batch has already gone through. Session cookie and token are blurred. -
Check the inbox
A single parallel batch delivered two dozen verification emails.
24 verification emails from WhiteMarket in one grouped thread, from a single burst.
Why it matters
There is no account takeover or data leak, which is why it is rated Low. The value to an attacker is spam and cost, on demand:
- Email bombing. Any verified address can be flooded with dozens of emails per burst, which is a nuisance and a soft denial of service on the inbox.
- Cost and reputation. Every message goes out through a paid transactional email service. At volume that runs up a bill, and a flood of mail from
white.marketrisks the domain being flagged as a spam source, which hurts real delivery. - Cover for other attacks. A wall of legitimate-looking emails is a classic way to bury a single important message, a password-reset or a security alert, under notification fatigue.
The fix
The counter has to be updated in the same atomic step as the check, so two requests can never both pass on the same allowance:
- Atomic rate limiting. Count with an atomic primitive, a Redis
INCR/SETNXwith TTL or a database advisory lock, so concurrent requests are serialized and counted correctly rather than racing. - One in-flight resend per user. Take a per-user mutex so only one resend can be processing at a time.
- Server-side cooldown. After a successful send, enforce a minimum interval before the next is allowed, checked atomically, not just reflected in the UI.
- Tighten the edge. The site sits behind Cloudflare, so add stricter per-IP and per-session rate-limit rules at the WAF as a second layer.
Key takeaways
- A limiter that returns 429 is not proof the limit holds. If it fires a beat late, a parallel burst has already slipped through, so always test with parallel requests.
- Check and increment must be a single atomic operation, or concurrent callers will split them.
- Low severity still means a real, repeatable cost, spam and reputation vector for the target.
- Edge rate limiting at a CDN is a useful second layer, but the fix has to live where the counter is.