Race Conditions: How Async Code Creates Security Holes AI Misses
Timing attacks that let attackers bypass limits and duplicate transactions
Race conditions let attackers exploit timing gaps between checking a condition and acting on it. Send 10 requests simultaneously to redeem a gift card 10 times. AI generates async code without proper locking, making this common in vibe coded apps. Fix: use database transactions and atomic operations. Classified as CWE-362.
What is a Race Condition?
A race condition occurs when two or more operations access shared resources concurrently, and the final result depends on timing. In web applications, this creates security vulnerabilities when attackers can exploit the gap between checking a condition and acting on it - known as the CHECK-USE gap.
Think of it like two people trying to book the last airline seat simultaneously. Both see "1 seat available," both click "Book," and both succeed - even though only one seat exists. In code, this manifests when you check a value (is balance sufficient?), then act on it (deduct balance) - but another request slips in between.
Normal single-user flow:
1. CHECK: Is coupon valid? → Yes
2. USE: Apply discount
3. UPDATE: Mark coupon as used
Race condition attack (two simultaneous requests):
Request A: CHECK (valid) → USE → UPDATE
Request B: CHECK (still valid!) → USE → UPDATE
↑
Gap exploited - coupon used twiceRace conditions fall under OWASP A04:2021 Insecure Design. A specific variant called TOCTOU (Time-of-check Time-of-use), documented as CWE-367, is particularly common in file operations and web applications.
Real-World Attack Examples
Race conditions enable financial fraud, privilege escalation, and security bypass. These attacks are effective because the vulnerability only appears under concurrent load - normal testing misses them entirely.
Gift Card Double Redemption
- Attacker has a $100 gift card code
- Opens 10 browser tabs to the redemption page
- Clicks "Redeem" simultaneously in all 10 tabs
- All 10 requests pass the "is valid" check before any marks it used
- Result: $100 gift card redeemed 10 times = $1,000 store credit
Rate Limit Bypass
- API allows 5 login attempts per minute
- Attacker sends 100 login requests simultaneously
- All requests hit the server before the rate counter updates
- 100 password guesses processed instead of 5
- This enables brute-force attacks on access control
Notable CVEs
- CVE-2024-50379 - Apache Tomcat RCE: TOCTOU race condition allowing remote code execution via file handling
- CVE-2025-68146 - Python filelock: Symlink attack via TOCTOU race condition in file locking library
- CVE-2021-0920 - Android Kernel: Use-after-free via race condition, actively exploited in the wild
Why AI Tools Generate Vulnerable Code
AI coding tools like Cursor and Claude Code generate async code that works perfectly for single users but breaks under concurrent load. The issue: AI focuses on functional correctness without considering concurrent access patterns.
// "Redeem coupon" - AI's typical output
app.post('/redeem', async (req, res) => {
const coupon = await Coupon.findOne({
code: req.body.code
})
if (!coupon || coupon.used) {
return res.status(400).json({
error: 'Invalid coupon'
})
}
// RACE WINDOW: Another request passes
// the check above while we're here
await applyDiscount(req.user, coupon.discount)
coupon.used = true
await coupon.save()
res.json({ success: true })
})// Atomic operation with database transaction
app.post('/redeem', async (req, res) => {
const result = await db.transaction(async (tx) => {
// Atomic check-and-update in one query
const coupon = await tx.coupon.update({
where: {
code: req.body.code,
used: false // Condition in the update
},
data: { used: true }
})
if (!coupon) {
throw new Error('Invalid or already used')
}
await tx.discount.create({
data: {
userId: req.user.id,
amount: coupon.discount
}
})
return coupon
})
res.json({ success: true })
})Why This Happens
- Training data: Most code samples are single-user scenarios
- await creates gaps: Every await point is a potential race window
- No concurrency model: AI doesn't think about parallel requests
- "It works" testing: Single-user tests always pass
Secure Patterns
The solution is always the same principle: eliminate the gap between check and use. Here are concrete patterns for common scenarios.
Fix 1: Database Transactions
Wrap related operations in a transaction. With Prisma:
// SECURE: Atomic transaction
async function withdrawFunds(userId, amount) {
await prisma.$transaction(async (tx) => {
const user = await tx.user.findUnique({
where: { id: userId }
})
if (user.balance < amount) {
throw new Error('Insufficient balance')
}
await tx.user.update({
where: { id: userId },
data: { balance: { decrement: amount } }
})
await tx.transaction.create({
data: { userId, amount, type: 'withdrawal' }
})
})
}Fix 2: Atomic Update with Condition
Combine check and update into single query. With MongoDB:
// SECURE: Single atomic operation
async function withdrawFunds(userId, amount) {
const result = await User.updateOne(
{
_id: userId,
balance: { $gte: amount } // Condition in query
},
{
$inc: { balance: -amount } // Atomic decrement
}
)
if (result.modifiedCount === 0) {
throw new Error('Insufficient balance or user not found')
}
}Fix 3: Database Unique Constraints
Let the database enforce uniqueness - don't check in application code:
// SECURE: Database enforces uniqueness
async function redeemCoupon(code, userId) {
try {
await Redemption.create({
couponCode: code,
userId
})
} catch (error) {
if (error.code === 11000) { // MongoDB duplicate key
throw new Error('Coupon already redeemed')
}
throw error
}
}Testing for Race Conditions
Race conditions are notoriously hard to find because they depend on timing. Normal testing with a single user will never reveal them. The PortSwigger race conditions guide covers testing techniques in depth.
Simple Test Script
// Simple race condition test
async function testRaceCondition() {
const requests = Array(20).fill().map(() =>
fetch('/api/redeem', {
method: 'POST',
body: JSON.stringify({ code: 'GIFT100' }),
headers: { 'Content-Type': 'application/json' }
})
)
const results = await Promise.all(requests)
const successes = results.filter(r => r.ok).length
console.log(`${successes} successful redemptions`)
// If > 1, race condition exists
}Code Review Patterns
Search your codebase for these patterns:
if (await ...check...) {...await ...update...}- check-then-actfindByIdfollowed bysave()- read-modify-write- Rate limiting without atomic counters
- Any code that checks availability, then reserves
AI Fix Prompt for Race Conditions
Copy this prompt to your AI coding tool to audit your codebase for race condition vulnerabilities:
This prompt guides Cursor, Claude Code, or other AI tools through systematic detection of check-then-act patterns, non-atomic operations, and missing transaction boundaries.
Frequently Asked Questions
What is a race condition vulnerability?
A race condition vulnerability occurs when two or more operations access shared data concurrently, and the final result depends on timing. In web applications, this means attackers can send multiple requests simultaneously to exploit the gap between checking a condition and acting on it. For example, redeeming a gift card 10 times by clicking simultaneously in 10 browser tabs.
How do attackers exploit race conditions?
Attackers send multiple requests simultaneously to hit the vulnerable window before state updates. Tools like Burp Suite's Turbo Intruder can send hundreds of requests in parallel. The single-packet attack technique bundles multiple HTTP requests into one TCP packet, ensuring they arrive at the same instant. Common targets: gift card redemption, discount codes, rate limits, and balance transfers.
How do I prevent race conditions in Node.js?
Use database transactions and atomic operations. With Prisma: prisma.$transaction(). With MongoDB: use $inc operator and conditions in queries. For critical operations, use database-level unique constraints to prevent duplicates. Never separate check from update - combine them into single atomic operations like UPDATE WHERE balance >= amount.
What is TOCTOU?
TOCTOU (Time-of-check Time-of-use) is a specific type of race condition (CWE-367) where a resource is checked and then used, but changes between those two steps. Example: checking if a file exists, then reading it - an attacker could swap the file between check and read. In web apps, this happens when validating data in one query and using it in another.
Are race conditions hard to detect?
Yes, race conditions are notoriously difficult to detect because they depend on timing and may not appear in normal testing. Single-user testing almost never reveals them. You need concurrent load testing or specialized tools. Code review patterns to look for: check-then-act sequences, non-atomic increments, and any gap between reading and writing shared state.
Related Security Topics
Scan Your Code for Race Conditions
vibeship scanner detects check-then-act patterns, non-atomic operations, and other race condition vulnerabilities in your AI-generated code.
Scan Your Repository