← Writeups

Racing the rate limit: unlimited OTP emails on LeveX

The endpoint that sends a one-time code capped how often you could call it. The cap was not atomic, so firing the same request in parallel slipped a whole burst past it and flooded the mailbox, turning the email provider's bill into the impact.

LeveX is a crypto exchange. In account settings, disconnecting a linked Google account asks you to confirm with a one-time code (OTP) sent to your email. The endpoint that sends that code is rate limited, so a normal user cannot spam it. The limit, though, was not enforced atomically: sending many copies of the request at the same instant let a large burst through at once. Each one sends a real email, so an attacker could make the platform send effectively unlimited OTP mail. Reported to LeveX on HackenProof, rated Low, and resolved.

The HackenProof report LEVEXWM-75 on the LeveX Web & Mobile program: state Resolved, severity Low, category Race Conditions, with M1S0 as the verified author
The report on the LeveX Web & Mobile program, LEVEXWM-75: Resolved, severity Low, category Race Conditions.

What a race condition is, in plain terms

A rate limit usually works in two steps: it reads a counter to check how many requests you have made recently, and if you are under the limit it lets the request through and adds one to the counter. That is fine when requests arrive one after another.

The problem is the tiny gap between the check and the update. If many requests arrive at the exact same moment, they can all read the counter before any of them has increased it. Every one of them sees "you are under the limit" and every one is allowed through. This is a race condition, sometimes called a time-of-check to time-of-use bug: the value that was checked is already stale by the time it is used.

Why parallel is the trick

Sent one at a time, each request updates the counter before the next is checked, and the limit holds. Sent all at once, they overlap in that gap and slip through together. The attack is not sending more requests, it is sending them at the same time.

The attack

  1. Reach the confirmation step

    In account settings, start disconnecting the linked Google account. It prompts for a code sent to your email.

    The LeveX Disconnect Account Authentication popup asking for an email code to confirm disconnecting a linked third-party account
    The confirmation prompt. Clicking to send the code is the request we are going to race. Emails are redacted.
  2. Capture the send-code request

    Send the request to Burp Suite. It is a single call, shown here with the session token and email removed:

    POST /service-user-identity/codes/mail HTTP/2
    Host: api100.levex.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:143.0) Gecko/20100101 Firefox/143.0
    Accept: */*
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate, br
    Referer: https://levex.com/en/my/settings/account
    X-Lx-Uuid: 4076083e-8dd0-40a3-2995-286f55cabe08
    X-Lx-Device: WEB
    X-Lx-Request-Tracing: 9c4981ff-20c9-6bf8-a186-dfb17b6117f4
    X-Authorization: [redacted]
    Content-Type: application/json
    Content-Length: 55
    Origin: https://levex.com
    Sec-Fetch-Dest: empty
    Sec-Fetch-Mode: cors
    Sec-Fetch-Site: same-site
    Priority: u=0
    Te: trailers
    
    {"email":"[redacted]","type":"ThirdAuthUnbind"}
    

    Under normal use the response returns {"error":null,"result":"OK"} and reports headers like X-Capacity: 500 with an X-Remaining that counts down, so a limiter clearly exists. It just does not survive concurrency.

  3. Fire it in parallel

    Put many copies of the identical request into a group and use Burp's send group in parallel (single-packet / last-byte sync). They arrive together and land in the gap between the limiter's check and its update.

    Burp Suite sending a group of about 31 identical OTP-request packets in parallel, each returning error null and result OK, showing the rate limit was bypassed
    A group of identical requests sent in parallel. Each returns result: "OK" and a code is sent, far past what the limiter should allow in that window. Token and cookies redacted.

The mailbox fills with verification codes from a single burst, even though the per-user limit is supposedly 500 and should never be reachable by hand.

A Gmail inbox filled top to bottom with LeveX verification code emails from the parallel burst
The result in the inbox: a wall of LeveX verification-code emails from one parallel burst.

Why it matters

There is no account takeover or data leak here, which is why it is rated Low. What it is, is a way to make the platform spend money and send spam on demand:

The fix

The limit has to be enforced as one atomic operation, so two requests can never both pass on the same allowance:

Key takeaways

  • A rate limit that is not atomic is not a rate limit. If check and increment are two steps, parallel requests slip between them.
  • Seeing X-Remaining count down proves a limiter exists, not that it holds under concurrency. Always test the limit with parallel requests, not just fast sequential ones.
  • Low severity does not mean no impact: an OTP-send race is a direct, repeatable cost and spam vector for the target.
  • Fix it where the counter lives, with an atomic increment or a per-target lock, not with client-side or after-the-fact checks.
Race Condition Rate Limit Bypass OTP API
Share on X

More writeups