Race Conditions
Race Conditions at a glance
Overview
Race conditions happen when code reads state, makes a decision, and writes new state while another concurrent request does the same. Time-of-check to time-of-use bugs allow bypassing authorization and business limits. Without atomic operations, locks, or idempotency, systems can double charge, oversell inventory, or leak files.
Where it occurs
It occurs in operations like inventory updates, coupon use, payments, or file downloads that lack proper concurrency controls, especially in microservices using retries or eventual consistency.
Impact
Financial loss from double charges or oversells, privilege escalation when checks and uses are split, data corruption, and confusing customer states that are hard to recover.
Prevention
Prevent race conditions by using atomic or transactional updates with proper isolation, idempotency keys, and deduplication, keeping checks and actions in the same critical section and binding operations to resolved resource handles.
Examples
Switch tabs to view language/framework variants.
Concurrent purchases allow credit balance to go negative
Two requests read the same balance and both succeed because update is non-atomic.
app.post('/buy', async (req,res)=>{
const user = await User.findById(req.user._id); // stale
const cost = Number(req.body.cost||0);
if (user.credits >= cost) {
user.credits -= cost; // race window
await user.save();
return res.json({ok:true});
}
res.status(402).json({ok:false});
});- Line 2: Separate read and write create a race window
Two concurrent handlers read the same balance and both decrement, causing an overdraft.
app.post('/buy', async (req,res)=>{
const cost = Number(req.body.cost||0);
const upd = await User.updateOne({ _id:req.user._id, credits: { $gte: cost } }, { $inc: { credits: -cost } });
if (upd.modifiedCount === 1) return res.json({ok:true});
return res.status(402).json({ok:false});
});- Line 2: Atomic conditional update ensures only one succeeds
Use a single atomic compare-and-update (findAndModify, update with predicate, or a transaction with proper isolation).
Engineer Checklist
-
Replace read-then-write with atomic updates or transactions
-
Use SELECT ... FOR UPDATE or document-level locks where supported
-
Introduce idempotency keys and dedupe for external actions
-
Keep authorization check and resource use inside the same critical section
-
Test with parallel clients and chaos tooling to surface races
End-to-End Example
A wallet service decrements credits after a separate read of the balance. Attackers send two purchase requests simultaneously. Both succeed and the balance goes negative.
// Node.js/Express + MongoDB - Vulnerable race condition
app.post('/api/purchase', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const itemCost = parseFloat(req.body.cost);
// VULNERABLE: Read-then-write without atomic operation
// Step 1: Read current balance
const user = await User.findById(userId);
const currentBalance = user.credits;
// Step 2: Check if user has enough credits
if (currentBalance < itemCost) {
return res.status(400).json({ error: 'Insufficient credits' });
}
// TIME GAP: Another request can execute here!
// If two requests run in parallel, both pass the check
// even if balance is only enough for one purchase
// Step 3: Deduct credits
const newBalance = currentBalance - itemCost;
await User.findByIdAndUpdate(userId, { credits: newBalance });
// Create purchase record
await Purchase.create({ userId, cost: itemCost });
res.json({ success: true, newBalance });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// ALSO VULNERABLE: Coupon redemption without atomic check
app.post('/api/redeem-coupon', authenticateToken, async (req, res) => {
const code = req.body.code;
const userId = req.user.id;
// VULNERABLE: Check and use in separate operations
const coupon = await Coupon.findOne({ code });
if (!coupon || coupon.used) {
return res.status(400).json({ error: 'Invalid coupon' });
}
// RACE WINDOW: Multiple requests can all pass the check!
// before any of them marks it as used
await Coupon.findByIdAndUpdate(coupon._id, { used: true, usedBy: userId });
await User.findByIdAndUpdate(userId, { $inc: { credits: coupon.value } });
res.json({ success: true, creditsAdded: coupon.value });
});// Node.js/Express + MongoDB - SECURE with atomic operations
app.post('/api/purchase', authenticateToken, async (req, res) => {
try {
const userId = req.user.id;
const itemCost = parseFloat(req.body.cost);
// SECURE: Atomic update with condition
// MongoDB's findOneAndUpdate with condition ensures atomicity
const result = await User.findOneAndUpdate(
{
_id: userId,
credits: { $gte: itemCost } // Condition: only update if enough credits
},
{
$inc: { credits: -itemCost } // Atomic decrement
},
{
new: true // Return updated document
}
);
// If result is null, condition failed (insufficient credits or race)
if (!result) {
return res.status(400).json({ error: 'Insufficient credits' });
}
// Create purchase record
await Purchase.create({ userId, cost: itemCost });
res.json({ success: true, newBalance: result.credits });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// SECURE: Coupon redemption with atomic check-and-set
app.post('/api/redeem-coupon', authenticateToken, async (req, res) => {
const code = req.body.code;
const userId = req.user.id;
// SECURE: Atomic update - only marks as used if currently unused
const coupon = await Coupon.findOneAndUpdate(
{
code: code,
used: false // Condition: only update if not already used
},
{
$set: { used: true, usedBy: userId, usedAt: new Date() }
},
{ new: true }
);
// If coupon is null, it was already used (race lost) or doesn't exist
if (!coupon) {
return res.status(400).json({ error: 'Invalid or already used coupon' });
}
// Atomic credit addition
await User.findByIdAndUpdate(userId, { $inc: { credits: coupon.value } });
res.json({ success: true, creditsAdded: coupon.value });
});
// ALTERNATIVE: Using transactions for multiple operations
app.post('/api/transfer-credits', authenticateToken, async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const fromUserId = req.user.id;
const toUserId = req.body.toUserId;
const amount = parseFloat(req.body.amount);
// Atomic debit
const fromUser = await User.findOneAndUpdate(
{ _id: fromUserId, credits: { $gte: amount } },
{ $inc: { credits: -amount } },
{ session, new: true }
);
if (!fromUser) {
await session.abortTransaction();
return res.status(400).json({ error: 'Insufficient credits' });
}
// Atomic credit
await User.findByIdAndUpdate(
toUserId,
{ $inc: { credits: amount } },
{ session }
);
await session.commitTransaction();
res.json({ success: true });
} catch (err) {
await session.abortTransaction();
res.status(500).json({ error: 'Transfer failed' });
} finally {
session.endSession();
}
});Discovery
This vulnerability is discovered by sending concurrent requests to endpoints that modify shared state (like balance transfers, coupon redemptions, or resource creation) and observing inconsistent results that indicate lack of proper synchronization.
-
1. Baseline sequential request test
httpAction
Perform normal sequential operations to establish expected state transitions
Request
POST https://app.example.com/api/purchaseHeaders:Content-Type: application/jsonAuthorization: Bearer user_token_abcBody:{ "item_id": "123", "quantity": 1, "cost": 50 }Response
Status: 200Body:{ "message": "Purchase successful", "balance_before": 100, "balance_after": 50, "transaction_id": "txn_001", "note": "Sequential operation correctly decrements balance by 50" }Artifacts
http_response_status account_balance transaction_record -
2. Concurrent request race detection
httpAction
Send multiple identical requests simultaneously to test for race conditions
Request
POST https://app.example.com/api/purchaseHeaders:Content-Type: application/jsonAuthorization: Bearer user_token_abcBody:{ "item_id": "456", "quantity": 1, "cost": 50, "note": "Send 3 identical requests in parallel with balance=100" }Response
Status: 200Body:{ "parallel_results": [ { "request": 1, "status": "success", "balance_read": 100, "balance_written": 50 }, { "request": 2, "status": "success", "balance_read": 100, "balance_written": 50 }, { "request": 3, "status": "success", "balance_read": 100, "balance_written": 50 } ], "final_balance": 50, "expected_balance": -50, "note": "Race condition! All 3 requests read balance=100 and wrote 50. Final balance should be -50 but is 50 - lost 2 updates!" }Artifacts
http_response_bodies final_balance transaction_count database_audit_log -
3. Coupon/voucher reuse test
httpAction
Attempt to redeem same coupon code in parallel requests
Request
POST https://app.example.com/api/redeem-couponHeaders:Content-Type: application/jsonAuthorization: Bearer user_token_xyzBody:{ "coupon_code": "SAVE20", "order_id": "order_789", "note": "Send 5 parallel redemption requests" }Response
Status: 200Body:{ "redemption_results": { "parallel_requests": 5, "successful_redemptions": 5, "expected_redemptions": 1, "total_discount_applied": "$100", "expected_discount": "$20", "revenue_loss": "$80" }, "note": "Single-use coupon redeemed 5 times due to TOCTOU race in validation!" }Artifacts
redemption_records discount_applied_count database_state -
4. TOCTOU file access race
httpAction
Test time-of-check to time-of-use vulnerability in file access controls
Request
GET https://app.example.com/api/download?file_id=sensitive_doc_456Response
Status: 200Body:{ "file_access_sequence": [ { "step": 1, "action": "permission_check", "timestamp": "2024-01-15T10:00:00.100Z", "result": "access_granted", "file_owner": "attacker_user" }, { "step": 2, "action": "symlink_swap", "timestamp": "2024-01-15T10:00:00.150Z", "note": "Attacker replaces file_id=456 with symlink to /etc/passwd during 50ms gap" }, { "step": 3, "action": "file_read", "timestamp": "2024-01-15T10:00:00.200Z", "content": "root:x:0:0:root:/root:/bin/bash\npostgres:x:999:999::/var/lib/postgresql:/bin/sh", "note": "Read sensitive system file due to TOCTOU race" } ], "vulnerability": "Permission checked on attacker-owned file, but content read from swapped target" }Artifacts
file_access_logs permission_check_timing file_content
Exploit steps
An attacker exploits this by sending parallel requests that exploit timing windows between check-and-use operations, allowing actions like redeeming the same coupon multiple times, withdrawing more funds than available, or bypassing rate limits.
-
1. Double-spend attack via concurrent purchases
Exploit balance race condition
httpAction
Send parallel purchase requests to spend same balance multiple times
Request
POST https://app.example.com/api/purchaseHeaders:Content-Type: application/jsonAuthorization: Bearer attacker_tokenBody:{ "item_id": "premium_item", "cost": 100, "note": "Send 3 parallel requests with balance=100" }Response
Status: 200Body:{ "attack_results": { "starting_balance": 100, "parallel_purchases": 3, "items_obtained": [ "premium_item_copy_1", "premium_item_copy_2", "premium_item_copy_3" ], "total_cost": 300, "final_balance": -100, "fraud_value": "$200" }, "note": "Successfully purchased $300 worth of items with only $100 balance via race condition" }Artifacts
transaction_records negative_balance purchased_items -
2. Coupon abuse through parallel redemption
Redeem single-use coupon multiple times
httpAction
Exploit TOCTOU in coupon validation to reuse promotional codes
Request
POST https://app.example.com/api/apply-discountHeaders:Content-Type: application/jsonAuthorization: Bearer attacker_tokenBody:{ "coupon": "ONETIME50", "order_id": "order_12345", "note": "Send 10 parallel requests" }Response
Status: 200Body:{ "coupon_abuse_results": { "coupon_code": "ONETIME50", "intended_discount": "$50", "redemptions": 10, "total_discount": "$500", "original_order_value": "$1000", "final_order_value": "$500", "revenue_loss": "$450" }, "note": "Single-use $50 coupon redeemed 10 times, saving $500 instead of $50" }Artifacts
discount_applications final_price coupon_usage_count -
3. Inventory overselling via concurrent orders
Purchase limited inventory concurrently
httpAction
Exploit race in inventory checks to oversell limited stock items
Request
POST https://app.example.com/api/orderHeaders:Content-Type: application/jsonBody:{ "product_id": "limited_edition", "quantity": 1, "note": "Product has stock=1, send 10 parallel orders" }Response
Status: 200Body:{ "overselling_results": { "product": "limited_edition", "available_stock": 1, "successful_orders": 10, "oversold_quantity": 9, "confirmed_shipments": [ "order_001", "order_002", "order_003", "order_004", "order_005", "order_006", "order_007", "order_008", "order_009", "order_010" ], "business_impact": "9 customers will not receive items, support tickets incoming" }, "note": "Race condition allowed 10 orders for 1 item in stock" }Artifacts
order_confirmations inventory_count oversold_quantity -
4. Privilege escalation via role assignment race
Exploit concurrent permission checks
httpAction
Race between permission check and role assignment to escalate privileges
Request
POST https://app.example.com/api/grant-roleHeaders:Content-Type: application/jsonAuthorization: Bearer attacker_tokenBody:{ "user_id": "attacker_123", "role": "admin", "note": "Send 5 parallel role grant requests" }Response
Status: 200Body:{ "privilege_escalation": { "original_role": "user", "requested_role": "admin", "parallel_requests": 5, "race_window": "15ms between permission check and role write", "final_role": "admin", "admin_panel_access": "https://app.example.com/admin", "note": "TOCTOU in authorization check allowed unprivileged user to grant admin role" } }Artifacts
user_roles permission_escalation audit_logs
Specific Impact
Customers obtain goods or services without being charged correctly. Accounting and reconciliation are wrong and support volume increases.
Fixing negative balances requires manual adjustments and undermines trust in the billing system.
Fix
Perform a single conditional update or hold row locks during the check and decrement. Add idempotency keys to external actions and keep checks and mutations in one transaction.
Detect This Vulnerability in Your Code
Sourcery automatically identifies race conditions vulnerabilities and many other security issues in your codebase.
Scan Your Code for Free