Guide to Identifying and Exploiting TOCTOU Race Conditions in Web Applications

18 min readApr 4, 2025

Before diving in, if you haven’t checked out my last article on how I hacked ServiceNow’s AI Agent and dumped 128K records — give it a read! 🔥
And if we’re not connected on LinkedIn yet, feel free to send me a request!

Introduction to TOCTOU Race Conditions

Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities are a specific type of race condition where an application checks a condition and later uses the result of that check, assuming nothing changed in between​. In a TOCTOU race, an attacker exploits the tiny time gap (the race window) between the check and the use, manipulating the state so that the original check is invalidated​. For example, the application might verify a user’s permissions or the validity of a token (time-of-check) and then proceed with an action (time-of-use) — if an attacker can change something between those two events, the action might be performed under false pretenses. These bugs often lead to unexpected behavior or security bypasses because the system ends up operating on outdated or manipulated information​

Race conditions are notoriously tricky to detect and exploit. They require precise timing, often by sending concurrent requests or performing parallel actions. A successful exploit can be powerful — enabling anything from financial fraud (like double-spending credits) to privilege escalation or account takeover. As bug bounty hunters and penetration testers, understanding TOCTOU patterns in web apps allows us to uncover subtle logic flaws that automated scanners or ordinary testing might miss​.

https://portswigger.net/web-security/race-conditions

How TOCTOU Manifests in Web Applications

In web applications, TOCTOU race conditions typically involve multi-step processes or shared resources that are accessed by multiple threads or requests. Unlike traditional low-level TOCTOU (such as file system races in OS programs), web-app TOCTOU issues often appear in application logic flows and database updates:

  • Multi-step workflows: When an operation spans multiple HTTP requests or stages (for example, request a token → verify token → perform action), a race condition may occur if two such workflows overlap in an unintended way.
  • Shared resources: If two requests modify the same data (e.g. user balance, coupon usage flag) concurrently, and the code doesn’t lock or serialize those operations, the checks may become invalid. This is essentially a TOCTOU if one request’s check/validation is undermined by another request’s actions.

Consider a simple scenario: an e-commerce site checks that a coupon code hasn’t been used, then applies a discount and marks the coupon as used. If an attacker quickly submits two requests in parallel with the same coupon, both might pass the “not used” check before either marks it used — resulting in the coupon being applied twice. The check (coupon valid?) is out-of-date by the time of use (applying discount) in one of the requests. This is the kind of vulnerability we’ll learn to find and exploit.

That scenario is real, and it happened at a +billion dollar company:

Common Attack Surfaces for TOCTOU in Web Apps

TOCTOU race bugs can surface anywhere the application performs a check then an action.

TOCTOU are all about perfect timing.

Let’s see several high-impact areas:

Race Conditions in Password Reset and Account Changes

Password reset flows are a fertile ground for TOCTOU races because they often involve multiple steps and tokens. A typical password reset process:

  1. User requests a reset; server generates a reset token tied to the account (time-of-check: “Okay, we’ll allow a reset for this user”) and usually stores something server-side (like in a session or database)
  2. User receives the token (via email) and sends it back with a new password to complete the reset (time-of-use: “Use the token to actually reset password”).

An attacker can exploit any mismatch in these steps. For example, a bug bounty report demonstrated how an attacker could reset another user’s password by racing two requests in one session:

  • The attacker first requests a reset for their own account, getting a token (this might store reset_token = X and username = attacker in the session).
  • Immediately, the attacker requests a reset for the victim’s username, using the same session. The application (perhaps using the session to track resets) overwrites the stored username to “victim” but does not generate a new token quickly enough or at all in that race condition​
  • Now the token X (which was emailed to the attacker) is mistakenly associated with the victim’s account on the server side. When the attacker uses token X, the server lets them set a new password for the victim’s account:
First request to reset password for "attacker"
POST /reset_password HTTP/1.1
Host: tesla.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
username=facundo@fernandez.com

Second request to reset password for "victim"
POST /reset_password HTTP/1.1
Host: tesla.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 26
username=saydi@fernandez.com

By sending these requests rapidly in succession, the server is tricked into associating the attacker’s token with the victim’s account. This is possible due to the server’s failure to handle simultaneous requests correctly, allowing the session data to be overwritten in an unintended manner.

In this scenario, the time-of-check is the issuance/validation of the token, and the time-of-use is applying the token to a username. The race condition let those get mismatched. (This actually happened due to the application using a single session storage for the reset token — a design flaw exploited by rapid dual requests.)

Another real-world example: Drupal once had a vulnerability where a “one-time” password reset link could be used twice if done quickly​. Normally, after the first use, the token is invalidated. But by sending two parallel requests with the same token, an attacker could have one request sneak through before the invalidation from the other took effect. This violated the expectation that a reset link can only be used once leading to a business logic error.

Password change flows (changing your password while logged in) can also have races. Consider if the application verifies the old password, then updates to the new password. If an attacker can hit the endpoint twice in succession (perhaps by intercepting the request and replaying quickly), one could imagine weird outcomes (like the account ending with a password the attacker knows). While less common than reset races, it’s worth testing change endpoints by sending multiple requests: one with correct old password and one with an incorrect one, for instance — to see if any logic breaks or if the wrong one might slip through due to timing.

Account modifications such as email change verification can be vulnerable too. A recent bug bounty case leveraged a race in a multi-step login with email OTP (one-time password). The application had a flow:

init login → send OTP → verify OTP

The attacker found they could start the flow with their own email, get an OTP, but then concurrently switch the email to the victim’s before OTP verification. The backend threads processed one request setting the email for the session to the victim, while another thread accepted the OTP for the session​. This resulted in the OTP (originally sent to attacker’s email) being accepted for the victim’s account, effectively logging in the attacker as the victim​.

The attacker only needed the victim’s email address to pull off this account takeover, no prior access to the account​. The flaw was that the system didn’t lock the flow to one email; it checked the OTP against the session’s user after allowing an update to that user. This earned a critical severity bounty for obvious reasons.

Key points to test in auth flows:

  • Can you initiate two reset or verification processes in parallel (same account or two different accounts)? Look for misbehaviors like one user’s token affecting another or a token being reusable.
  • Try using one token or code twice. If the second attempt succeeds (when it should have been invalidated), that’s a race.
  • For multi-step flows (like 2FA or email change confirmation), attempt to alter an identifier (email, username, phone number) mid-process, as the OTP example above illustrates. If the system isn’t carefully tracking state, you might confuse it.
  • Also, test concurrent logins or sessions: occasionally, if an app updates something like a login token or session ID during login, two parallel login attempts might end up elevating privileges or skipping a security step (though this is less common nowadays, it has happened).

Race Conditions in Payment and Coupon Systems

Perhaps the most infamous race condition scenarios in web apps involve financial or reward operations, often yielding what hackers gleefully call a “free money” bug. These typically occur in features like coupon redemption, gift cards, account balance transfers, or any process that subtracts or adds value under certain conditions.

Coupon & Gift Card Double-Spend: If an application doesn’t properly handle simultaneous use of a coupon or gift code, an attacker can redeem one code multiple times. In this disclosed Instacart report:

https://hackerone.com/reports/157996

A user could redeem the same coupon code repeatedly, stacking discounts beyond what was intended​. The backend likely checked “has this code been used?” and then applied a discount and flagged it as used, but concurrent requests meant the flagging happened twice only after both checks passed. This mirrors other bug bounty reports where gift cards could be used more than once via rapid concurrent requests​. Essentially, one thread uses the gift card while another thread still sees it as unused, letting both transactions go through, resulting in duplicate credit or free balance added for the attacker​.

Real-world examples:

  • A race condition in Dropbox’s coupon handling (from a 2015 report) allowed an attacker to reuse a single-use coupon code, getting multiple credits from one code​.
  • A vulnerability in an events platform “alf.io” (CVE-2024-???) showed that by racing the “apply promo code” step, one could bypass the usage limit on promo codes​
  • The attacker had to win two race windows (the code was validated twice during a ticket purchase flow), but by carefully synchronizing requests, they redeemed the discount twice for two separate tickets when only one use was allowed.
  • Even loyalty points or referral bonuses can be targeted. For instance, a vulnerability in Vend’s loyalty system let an attacker repeatedly trigger a “loyalty claim” action via race conditions, yielding far more bonus points/cash than intended (reportedly, the attacker could hit a special claim endpoint multiple times to accumulate rewards).

Payments and Money Transfers: Any time money moves or balances change, think about race conditions:

  • Bank transfers or wallet transactions: If an app subtracts money from one account and adds to another, two simultaneous transfers could each check the source account’s balance before either has deducted it. This might allow an account with $100 to send $100 twice, because both transactions saw $100 available. If the app doesn’t use a transaction or lock, the result could be $200 sent while the source only loses $100 (essentially creating $100 from nothing). This kind of bug has been used to steal money from online banks by attackers in the wild for years!!!​
  • Purchasing items or upgrading services: A bug in Reddit’s coin purchase flow (Reddit’s in-app currency) allowed users to inflate their coins by racing the purchase verification endpoint​
  • Likely, one request validated a purchase and granted coins while another (using the same receipt) did the same before the system realized the receipt was already used — giving double coins for one payment.
  • Avoiding limits: Race conditions can also simply bypass a limit counter. For example, if an API endpoint is supposed to allow one free trial or one free premium subscription per user, perhaps a well-timed parallel request can grant multiple. A bug on a dating app’s premium feature did exactly this, by racing the “get free premium” process, attackers obtained extra days of premium access for free beyond the normal one-time limit​

When testing payment flows you should:

  • Identify any unique code redemption, balance update, or one-time reward functionality.
  • Use a tool (or even browser dev tools) to send the same request multiple times very quickly. Observe if you get multiple successes. Often, you’ll need to intercept the request and replay it with a runner or script because doing it manually in the UI is too slow.
  • Check the effects on the backend data: Was a coupon applied twice? Did a balance decrease only once but increase twice somewhere else? Sometimes the HTTP response might not obviously show the bug (one might fail, for example), but the state change did happen twice. So double-check account states.
  • Also test multi-step purchase flows: e.g., initiate purchase and confirm payment simultaneously or cancel vs confirm at the same time, to see if you can get an item without paying or money refunded twice, etc.

File Operation Race Conditions

Web applications that interact with the file system (for example, uploading files, renaming files, or accessing server-side files) can introduce classic TOCTOU races. A typical example is when an app with elevated privileges checks file permissions or existence, then opens or writes to the file. If the filename or path can be influenced by an attacker (even indirectly), there’s a chance to swap the file in the brief moment between check and use.

Example — Symlink Race: A privileged backend script might do something like:

  • if (user_has_access(file))check permissions, then
  • open(file)use the file.

An attacker could replace file with a symbolic link after the permission check but before the open. In a classic illustration, the program checks a file that the attacker does have access to, but the attacker quickly repoints the filename to a sensitive file (via a symlink or rename) just before the open. The privileged program then opens the new target file, wrongly believing the access was already validated​. In essence, the state of the file changed between the check and use. This kind of TOCTOU flaw has led to serious issues like local privilege escalation in native applications.

Web App Context: In web apps, direct file system races are less common but still possible:

  • A web interface might allow an admin to download log files. If it checks that the requested filename is in an allowed directory, and then reads the file, an attacker might exploit a race by switching symlinks or filenames to trick the app into reading arbitrary files after the check. For instance, a race condition in Jenkins (a web-based CI platform) allowed reading arbitrary files from the server because the file browser plugin didn’t securely handle the check-then-open sequence​
  • File upload scanners provide another surface. Suppose an app saves an upload to a temp folder, scans it, then moves it to a user-accessible location. If the file path or content can be swapped in that interval (say by uploading two files quickly and manipulating identifiers), it could bypass security checks. (These are complex, but worth being aware of during testing.)

Takeaway: Whenever you see file operations with separate steps (especially in languages like C/C++ extensions or system calls made by a web app), consider TOCTOU. In practice, symlink attacks and quick file replacements are the go-to technique for exploiting these. As a pentester, if you can create or control files on the target (through uploads or local include), try to race the system by switching file references between a check and an action.

Strategies and Tools for Finding TOCTOU Race Conditions

Exploiting race conditions is an art of timing. Below are actionable steps and tools to systematically uncover and exploit these bugs:

1. Identify Suspicious Functionality

Start by mapping the application for features that might be race-prone:

  • Single-use tokens or codes: Password reset links, email confirmation links, one-time promo codes, single-use coupons, these all imply a one-and-done usage, which is a hint that a race might allow double usage.
  • State-changing actions with limits: Adding/removing something (like funds, likes, votes), actions that should only happen once per user or have a counter (apply discount, claim reward, submit a form once).
  • Multistep processes: Any workflow that involves multiple requests to complete (shopping cart checkout steps, multi-page forms, OTP or multi-factor login, etc.). These often have intermediate states that can be tampered with.

Review the application logic or ask the developers (if its a white-box engagement) where they do checks and updates separately. In black-box bug hunting, you infer this by looking at API patterns (e.g., a POST /init followed by POST /confirm – clearly a check then use sequence).

2. Probe for Race Windows (Timing Clues)

Once you suspect a functionality, manually perform the action and observe behavior:

  • Response times: Sometimes a race condition attempt yields unusual response patterns. For example, one request might hang a tad longer or an expected failure might sometimes succeed if timed just right. These can be subtle, but if you notice flakiness when clicking fast, investigate further.
  • Duplicate IDs or values: If doing something twice quickly returns two identical identifiers (like two orders with the same order number, or two password reset emails with the same token), that’s a red flag of non-atomic operations.
  • Logs or error messages: In some cases, an application might log a warning like “duplicate request” or an error about something already being used. If you have access to any such info (or the app surfaces an error to you), it can hint that a race was attempted internally.

At this stage, you can do quick manual tests like opening two browser windows and clicking a button in both as simultaneously as possible. This is crude, but if it yields any anomaly (say you see your account credited twice when you expected once), you know it’s worth automating a true exploit.

3. Use the Right Tools to Exploit the Race

Manual timing is rarely precise enough to reliably exploit race conditions. Professional hunters use specialized tools or scripts to fire multiple requests at the exact same moment. Here are a few approaches:

  • Burp Suite Intruder (with modifications): Burp Intruder by default sends requests sequentially, but you can use the Pitchfork mode with the same payload in multiple positions to try and send near-simultaneous requests. More effectively, the newer Burp Turbo Intruder extension is designed for race conditions. Turbo Intruder lets you script custom attack logic and can achieve very high speed and synchronized requests. For example, you can queue up a bunch of requests and then release them all at once using Turbo Intruder’s gate mechanism​
  • (The extension provides a Python API; you configure an engine and use engine.openGate('X') to unleash a batch of queued requests at the same time.) This can help maximize the chance that the server truly handles the requests in parallel.
  • Burp Repeater with HTTP/2: Burp added a feature for grouping requests and also supports an experimental “single-packet” technique for HTTP/2. If the target supports HTTP/2, you can send multiple HTTP/2 requests in one TCP packet, virtually guaranteeing simultaneous arrival​.
  • In Burp Repeater, you can achieve this by selecting multiple requests and using “Send group” (the group sending feature) which tries to send them together. This approach was crucial in the OTP bypass case — the researcher sent the email-change request and OTP verification request in one go to ensure the race condition triggered​
  • Keep in mind single-packet attacks only work over HTTP/2 and certain conditions, but they can eliminate issues of network jitter where one request might otherwise arrive a few milliseconds before the other.
  • Race condition fuzzers / scripts: There are open-source tools specifically for race conditions. Race The Web (by @TheHackerDev) is a popular one, which can send a flood of simultaneous HTTP requests and detect anomalies. One bug bounty writeup noted using Race The Web to send requests faster than Burp could
  • Race The Web is easy to configure: you supply a request template and number of threads, and it will bombard the target concurrently. Another tool is FFUF (for fuzzing) which isn’t built for races per se but can fire off bursts of requests if configured with a low delay and high concurrency. In some cases, testers write a simple Python or Bash script to launch requests in parallel (for instance, using Python’s threading or asyncio to start 10 requests at once). The key is to minimize the delay between the first and last request.
# Example: Using Python threads to attempt a race condition
import requests, threading

url = "https://example.com/api/redeem"
data = {"coupon": "SUMMER2025"}
def send_request():
resp = requests.post(url, data=data, cookies={"session":"...your session..."})
print(resp.status_code, resp.text)

threads = []
for i in range(5):
t = threading.Thread(target=send_request)
threads.append(t)
t.start()
for t in threads:
t.join()

The above script will fire 5 coupon redemption requests as close to simultaneously as the OS scheduler allows. Custom scripts can be very effective for simple cases and allow you to adjust timing (e.g., inserting a small sleep to try to widen/narrow the race window). Just be careful to respect any rate limiting or anti-automation logic the target may have.

  • Last-byte synchronization: This is an advanced trick to maximize simultaneity. The idea is to initiate each request but hold it right before the end (don’t send the final byte) so that the server has all requests “lined up” waiting for that last byte. Then you release all the final bytes at once, causing the server to begin processing all requests nearly concurrently. Tools like Turbo Intruder support this by letting you control packet streaming, and some custom scripts can achieve it with low-level socket programming. In one advisory, researchers used last-byte sync to get a stable exploit of a promo code race condition​
  • In Burp, the group send feature effectively does this under the hood for HTTP/1.1 by not completing any request until all are ready to go. Keep this technique in mind if simple concurrent threads aren’t triggering the bug due to slight timing differences.

When using these tools, send a high number of requests (to improve odds) but be mindful of not overwhelming the server to the point that it crashes or your requests queue up (which can actually prevent the race). A common strategy is to start with 10 or 20 parallel requests and see if you get a double execution, then dial up as needed. Often, two well-timed requests are enough for a basic race (one to interfere with the other), but sometimes you might use many to increase the chance of hitting the sweet spot.

4. Observe and Confirm the Race Condition

After firing your race attempts, carefully observe the results:

  • Did you receive two “success” responses when only one should happen? (E.g., two password reset emails, multiple confirmation messages, etc.)
  • Check the state on the server: Has something changed in the account or database? For example, if testing a money transfer, check balances after the race — did the money get duplicated or not fully deducted? If testing a coupon, see if the discount applied twice or if the coupon still shows as unused after one use (meaning the second thread likely won).
  • Sometimes the evidence is indirect. In the password reset token reuse case, you might only see that the second reset link still works when it shouldn’t. In an invite limit bypass, you might not see an obvious error, but you’ll notice you were able to create two new users instead of one. So, design your test to capture these effects (for instance, in an invite race, try using both invite links that get generated).

It’s often useful to log or print something from each thread (as in the Python example above, printing status code and maybe a snippet of response). If you see, for example, two threads both got “200 OK” where one was expected to fail, that’s a confirmation. In some cases, one response might show an error like “already used” but another succeeds — the timing might require multiple trials to get both succeeding. Repeat the test multiple times to be sure; race conditions can be fickle, and a successful exploit might only occur 1 in 10 tries depending on timing. Consistency isn’t guaranteed, but if you can hit it at least occasionally and explain the scenario, you have a valid bug.

Final thoughts

TOCTOU race conditions in web applications are a fascinating blend of logical vulnerability and exploitation skill. From file system quirks to business logic flaws in payments and auth, they require you to think about how a fast-moving attacker could slip between the cracks of an application’s intended sequence. As a bug bounty hunter, always keep an eye out for functionalities where “check” and “action” are separate — that’s your cue to attempt a race.

In this guide, we covered how to recognize these vulnerable patterns and attack surfaces (files, resets, logins, payments, etc.), drawing on real bug bounty cases for illustration — such as coupon codes reused beyond their limit​ or critical account takeovers via OTP manipulation​. We also discussed a methodology for testing: start with educated guesses, then leverage tools like Burp’s Turbo Intruder and Race The Web to fire off perfectly timed payloads. With practice, you’ll get a feel for crafting race condition exploits and interpreting their sometimes subtle outcomes.

Always remember to exploit race conditions responsibly. It can be tempting to, say, try a thousand concurrent requests to double your bug bounty reward points, but causing excessive load might tip off the defenders or crash the app (neither of which helps your case!). Instead, use just enough force to prove the concept and collect evidence. When reporting, include details like how many concurrent attempts were needed and the effect observed.

By understanding both the technical underpinnings (TOCTOU mechanics in different stacks) and the practical techniques (tools and steps to exploit), you’ll be well-equipped to race against the machines — and win. Good hunting!

If you enjoyed reading this make sure to give me 50+ claps and comment!

Feel free to send me a LinkedIn request!

--

--

Responses (1)