Blog · E.18 · Technical

Race condition & TOCTOU testing

Your app checks the balance, then makes the withdrawal. Between those two steps is a sliver of time - and if an attacker fires fifty requests at once, several can pass the check before any of them updates the balance. That's a race condition, and it's how a single-use coupon gets used a hundred times or an account goes overdrawn. Here's the mechanism, the money it moves, the single-packet attack that makes it reliable, and how to fix it for good.

Race ConditionTOCTOUDouble-SpendSingle-Packet AttackBusiness Logic
Race: Check-then-Act Window · Concurrent Requests · Double-Spend · Coupon / Limit Bypass · Single-Packet Attack (HTTP/2) · Fix = Atomic DB Operations Race: Check-then-Act Window · Concurrent Requests · Double-Spend · Coupon / Limit Bypass · Single-Packet Attack (HTTP/2) · Fix = Atomic DB Operations
// TL;DR

A race condition (often a TOCTOU - time-of-check-to-time-of-use - flaw) happens when an app checks a condition then acts on it, and an attacker sends many simultaneous requests so several pass the check before the state updates. Impact is usually financial: redeeming a single-use coupon/gift card repeatedly, double-spend beyond a balance, exceeding rate/quantity limits, farming loyalty points. The single-packet attack (via HTTP/2 multiplexing) synchronises request arrival, making tight race windows reliably exploitable and testable. It's a business-logic flaw scanners miss. The fix: make check-and-act atomic at the data layer - locking/transactions, atomic DB operations, unique constraints, idempotency keys. Detail below.

// 01 The check-then-act window

Most limits in an application are enforced in two steps: check a condition, then act on it. “Is the balance ≥ the withdrawal? Yes - deduct it.” “Has this coupon been used? No - apply it and mark it used.” Developers write this as if it happens one request at a time. But a web server handles many requests concurrently, and between the check and the act there is a tiny window. A race condition exploits that window: an attacker sends many requests at once, and several of them read the state, all pass the check, and all proceed to act before any of them has updated the state. The limit that was meant to fire once fires many times. In the classic framing this is a TOCTOU flaw - the state at time-of-check is no longer true at time-of-use.

// 02 Where the money leaks

01

Double-spend

Withdraw or transfer more than a balance allows - the same funds counted twice.

02

Coupon / gift-card abuse

Redeem a single-use discount, voucher or gift card many times over.

03

Limit bypass

Exceed quantity or rate limits - claim more of a limited item than permitted.

04

Bonus farming

Apply loyalty points, referral or signup bonuses repeatedly; defeat one-time actions.

Notice the theme: race conditions defeat exactly the limits that protect value. That's why they're rarely academic - a successful race usually converts straight into money or goods, which is why they land as high severity in e-commerce, fintech and any app with balances or quotas.

// 03 The single-packet attack

Race windows can be microscopic - a few milliseconds - so the historical problem for both attackers and testers was reliability: fire many requests and hope enough arrive within the window. Network jitter made that hit-or-miss. The single-packet attack solves it by removing timing jitter entirely. Instead of sending requests sequentially and praying, the tester arranges multiple requests so they reach the server effectively simultaneously - commonly by abusing HTTP/2 multiplexing to pack many requests into a single network packet so they're delivered and processed together. This synchronises arrival far more precisely than sequential requests, turning a flaky, hard-to-hit race into a reliably exploitable - and therefore reliably testable - condition. Modern tooling implements it, but recognising which flows are race-sensitive is the tester's judgement.

// 04 How to test for races

Testing is targeted, not blanket. The tester first identifies the sensitive flows - anywhere a limit, balance, quota, single-use token or one-time action exists: checkout and payment, withdrawals/transfers, coupon and gift-card redemption, voting, signup bonuses, quantity-limited purchases. Then, for each, they send a burst of concurrent identical requests (using the single-packet technique for tight windows) and check whether the limit was defeated - two withdrawals from one balance, a coupon applied twice, a quota exceeded. It's inherently manual and logic-driven: a scanner has no concept that a coupon should be single-use, so it can't test whether it can be used twice - the same reason business-logic flaws generally evade automation. Any confirmation is done carefully within the rules of engagement, since it touches real state.

// 05 How to fix it

The fix is a principle: any limit that matters must be enforced atomically by the data layer under concurrency - never by an application-level check a race can slip past. Concretely: use database transactions and locking so the check-and-act is a single, uninterruptible operation; prefer the database's atomic operations and unique constraints (e.g. an atomic conditional update, or a unique index that makes a second redemption fail) over reading-then-writing in code; add idempotency keys for operations that must happen exactly once, so duplicate concurrent requests collapse to one; and use rate limiting as a supporting - not primary - control. The mental model to instil in developers: assume every request runs at the same time as fifty others, and make the guarantee hold anyway. That's the standard our methodology reports findings against.

// 06 Frequently asked questions

What is a race condition in a web app?

When the outcome depends on the timing of concurrent actions the developer assumed would happen one at a time. It typically appears as a TOCTOU flaw: the app checks a condition (balance, coupon uses, limit) then acts, but an attacker sends many simultaneous requests so several pass the check before any updates the state - so a limit meant to fire once is bypassed multiple times.

What can an attacker do with one?

Usually something with direct financial impact: redeem a single-use discount or gift card multiple times, withdraw or transfer beyond a balance (double-spend), exceed rate or quantity limits, apply loyalty points or bonuses repeatedly, and bypass one-time actions. Because the limit protects value, defeating it usually translates into money or goods - hence high severity.

What is the single-packet attack?

A technique that improves race-testing reliability by removing network jitter. Instead of sending many requests and hoping they arrive together, the tester arranges them to reach the server simultaneously - commonly via HTTP/2 multiplexing to send many requests in one packet. This synchronises arrival far more precisely, making tight race windows reliably exploitable and testable.

How do you prevent race conditions?

Make the check-and-act atomic so concurrent requests can't interrupt it: database locking or transactions, the database's own atomic operations and unique constraints rather than read-then-write in code, idempotency keys for once-only operations, and rate limiting as support. Any limit that matters - balances, single-use codes, quotas - must be enforced by the data layer under concurrency, not an app-level check.

// 07 Related reading

UG

Usama Gul

Founder & Penetration Testing Lead, CyberFortify

Tests the check-then-act windows attackers turn into money — using single-packet techniques to prove a coupon reused or a balance double-spent, then prescribing the atomic data-layer fix.

Balances, coupons, quotas?

Anywhere your app checks a limit then acts on it can be raced. We test the money-moving flows with single-packet precision and hand you the atomic fix before an attacker double-spends.

Scope a web app test → Web app testing →