What is a Race Condition?
A race condition occurs when the correctness of a program depends on the relative timing or ordering of concurrent events. In web applications, attackers can exploit race conditions by sending multiple simultaneous requests to trigger operations that should only execute once — such as redeeming a coupon, withdrawing funds, or consuming a one-time token.
Time-of-Check to Time-of-Use (TOCTOU) is a specific subtype: the application checks a condition (e.g., “does the user have sufficient balance?”), then — before acting on the result — the shared state changes. An attacker races multiple requests through the check simultaneously, all passing before any deduction is applied.
How exploitation works
A discount coupon redemption endpoint:
1. Check: Is coupon "SAVE10" valid and unused? → Yes
2. [GAP — attacker fires 10 simultaneous requests]
3. Mark coupon as used → only updates once
4. Apply discount → applied 10 times
All 10 concurrent requests pass step 1 before any of them execute step 3, allowing the coupon to be applied multiple times. Similar attacks are used against loyalty point systems, file-based locks, and limited-inventory purchases.
Vulnerable code examples
C# / ASP.NET Core — non-atomic check-and-update
// VULNERABLE: Read and update are separate, non-atomic operations
[HttpPost("redeem")]
public async Task<IActionResult> RedeemCoupon(string code, string userId)
{
var coupon = await _db.Coupons.FindAsync(code);
if (coupon == null || coupon.IsUsed) return BadRequest("Invalid coupon");
// RACE: Another request can pass this check before this update executes
coupon.IsUsed = true;
coupon.UsedBy = userId;
await _db.SaveChangesAsync();
await ApplyDiscount(userId, coupon.Value);
return Ok();
}
Node.js — filesystem TOCTOU
// VULNERABLE: Check then use — file could change between stat() and readFile()
async function processConfig(filePath) {
const stat = await fs.stat(filePath);
if (stat.size > MAX_SIZE) throw new Error('File too large');
// RACE: attacker replaces file with symlink between stat and readFile
const content = await fs.readFile(filePath, 'utf8');
return parseConfig(content);
}
Secure code examples
C# — atomic database update
// SECURE: Single atomic UPDATE with WHERE condition prevents double redemption
[HttpPost("redeem")]
public async Task<IActionResult> RedeemCoupon(string code, string userId)
{
// Atomic: only succeeds if IsUsed is currently false
var updated = await _db.Database.ExecuteSqlRawAsync(
"UPDATE Coupons SET IsUsed=1, UsedBy=@userId WHERE Code=@code AND IsUsed=0",
new SqlParameter("@userId", userId),
new SqlParameter("@code", code));
if (updated == 0) return BadRequest("Coupon already used or invalid");
await ApplyDiscount(userId, await GetCouponValue(code));
return Ok();
}
Java — optimistic locking with database versioning
// SECURE: Optimistic locking — update fails if another transaction modified the record
@Entity
public class Coupon {
@Version
private long version; // JPA increments this on every update
private boolean used;
// ...
}
// Service: ObjectOptimisticLockingFailureException thrown on concurrent update
@Transactional
public void redeemCoupon(String code, String userId) {
Coupon coupon = couponRepository.findByCode(code)
.filter(c -> !c.isUsed())
.orElseThrow(() -> new InvalidCouponException());
coupon.setUsed(true);
couponRepository.save(coupon); // Throws if version conflict
}
What Offensive360 detects
- Non-atomic check-then-act patterns — Read-then-write sequences on shared state without locking or atomic operations
- Missing database-level constraints — Unique constraints or conditional updates absent from critical resources
- Filesystem TOCTOU —
stat()/exists()calls followed byopen()/read()on user-influenced paths - Unprotected counters/balances — Increment/decrement operations on shared numeric values outside transactions
- Missing idempotency controls — Payment or action endpoints lacking request deduplication keys
Remediation guidance
-
Use atomic database operations — Perform check-and-update as a single SQL statement with a
WHEREcondition (e.g.,UPDATE ... WHERE status='pending') and check the affected row count. -
Use optimistic or pessimistic locking — ORMs support row-level locking (
SELECT FOR UPDATE) and version-based optimistic locking (@Version,rowversion). -
Apply unique database constraints — Enforce business rules at the database level so duplicate operations fail at the storage layer regardless of application logic.
-
Implement idempotency keys — For payment and redemption endpoints, accept a client-supplied idempotency key and deduplicate requests at the application level.
-
Use distributed locks for cross-service operations — When coordinating across microservices, use Redis
SET NXor equivalent distributed locking primitives.