Race Condition Vulnerabilities in Web Applications
A race condition happens when the outcome depends on the timing of concurrent requests. In web applications, attackers send many requests at almost the same moment to slip through the gap between a check and an action.
The classic pattern: check, then act
// Vulnerable: two requests can both pass the check before either updates
const user = await db.user.findUnique({ where: { id } });
if (user.balance >= amount) {
await db.user.update({ where: { id }, data: { balance: user.balance - amount } });
await sendPayout(amount);
}
If an attacker sends twenty withdrawal requests simultaneously, several may read the same balance, pass the check and pay out, overdrawing the account.
What attackers exploit with races
- Redeeming a gift card or coupon multiple times.
- Withdrawing or transferring more than the balance.
- Exceeding limits: seats, invitations, free trials or API quotas.
- Bypassing rate limits on login or one-time codes by sending attempts in parallel.
- Voting or liking multiple times.
- Double processing of webhooks or jobs.
Why races are easier to exploit than they seem
Attackers use techniques that send requests so close together that the server processes them concurrently, such as sending the final bytes of many prepared requests at once over a single connection. Modern HTTP/2 makes it practical to land many requests within a millisecond window.
How to prevent race conditions
1. Make the check and the update one atomic operation.
UPDATE accounts SET balance = balance - $1
WHERE id = $2 AND balance >= $1
RETURNING balance;
If no row is returned, the withdrawal fails. The database guarantees only valid updates succeed.
2. Use unique constraints for one-time actions. A unique index on (coupon_id, user_id) makes a second redemption fail regardless of timing.
3. Use transactions with appropriate isolation, or row-level locks such as SELECT ... FOR UPDATE, for multi-step operations.
4. Use idempotency keys for payments and webhooks, so duplicate requests have no extra effect.
5. Use distributed locks carefully for operations spanning services, with timeouts and fencing.
6. Make rate limiting atomic, for example using an atomic increment in Redis rather than read-then-write.
Where to look in your code
Review any code that reads a value, makes a decision, then writes: balances, counters, inventory, quotas, one-time tokens and status transitions. These are prime candidates.
Testing for race conditions
Testing requires sending carefully synchronised parallel requests and checking whether limits were exceeded. SecurityInfinity agents identify limit-enforcing endpoints and test them for concurrency flaws. Start free.
Frequently asked questions
Do race conditions only affect financial apps?
No. Any limit, one-time action or state transition can be affected, including invitations, trials and approvals.
Are race conditions hard to reproduce?
They can be intermittent, but synchronised request techniques make exploitation surprisingly reliable.
Does using a single-threaded runtime like Node.js prevent races?
No. Node.js handles requests concurrently with asynchronous I/O, so check-then-act patterns across await points are still vulnerable.