Race conditions are among the most insidious bugs in software development. They're hard to reproduce, hard to test, and can have devastating consequences—especially in payment systems where they can lead to double-spending vulnerabilities.
What is TOCTOU?
TOCTOU stands for "Time-of-Check to Time-of-Use." It occurs when a program checks a condition, then acts based on that condition, but the condition changes between the check and the action.
A Real-World Example
Consider a gift card redemption system:
// Vulnerable to TOCTOU race condition
async function redeemGiftCard(cardId: string, amount: number, userId: string) {
// Time of Check
const card = await GiftCard.findById(cardId);
if (card.balance < amount) {
throw new Error('Insufficient balance');
}
// Time of Use - balance could have changed!
card.balance -= amount;
await card.save();
await creditUserAccount(userId, amount);
}
The problem? Between checking the balance and updating it, another request could have reduced the balance. With perfect timing, an attacker with a $100 gift card could redeem it twice.
The Attack Scenario
Request 1 Request 2
─────────────────────────────────────────────────
Check balance: $100 ✓
Check balance: $100 ✓
Deduct $100
Deduct $100
Credit user $100
Credit user $100
Result: User gets $200 from a $100 card
Solutions
1. Optimistic Locking with Version Numbers
async function redeemGiftCard(cardId: string, amount: number, userId: string) {
const card = await GiftCard.findById(cardId);
if (card.balance < amount) {
throw new Error('Insufficient balance');
}
// Update only if version matches
const result = await GiftCard.updateOne(
{
_id: cardId,
version: card.version, // Optimistic lock
balance: { $gte: amount },
},
{
$inc: { balance: -amount, version: 1 },
}
);
if (result.modifiedCount === 0) {
throw new Error('Card was modified, please retry');
}
await creditUserAccount(userId, amount);
}
2. Pessimistic Locking with SELECT FOR UPDATE
async function redeemGiftCard(cardId: string, amount: number, userId: string) {
await db.transaction(async (tx) => {
// Lock the row for the duration of the transaction
const card = await tx.query(
'SELECT * FROM gift_cards WHERE id = $1 FOR UPDATE',
[cardId]
);
if (card.balance < amount) {
throw new Error('Insufficient balance');
}
await tx.query(
'UPDATE gift_cards SET balance = balance - $1 WHERE id = $2',
[amount, cardId]
);
await tx.query(
'INSERT INTO user_credits (user_id, amount) VALUES ($1, $2)',
[userId, amount]
);
});
}
3. Atomic Operations
async function redeemGiftCard(cardId: string, amount: number, userId: string) {
// Single atomic operation - no race window
const result = await GiftCard.findOneAndUpdate(
{
_id: cardId,
balance: { $gte: amount },
},
{
$inc: { balance: -amount },
},
{ new: true }
);
if (!result) {
throw new Error('Insufficient balance or card not found');
}
await creditUserAccount(userId, amount);
}
Other Common TOCTOU Patterns
Inventory Systems
// Vulnerable
const product = await Product.findById(productId);
if (product.stock < quantity) throw new Error('Out of stock');
product.stock -= quantity;
await product.save();
// Fixed
const result = await Product.updateOne(
{ _id: productId, stock: { $gte: quantity } },
{ $inc: { stock: -quantity } }
);
if (result.modifiedCount === 0) throw new Error('Out of stock');
Account Balance Checks
// Vulnerable
const account = await Account.findById(accountId);
if (account.balance < withdrawAmount) throw new Error('Insufficient funds');
await transfer(accountId, destinationId, withdrawAmount);
// Fixed - use database transaction with row locking
await db.transaction(async (tx) => {
const account = await tx.query(
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
[accountId]
);
if (account.balance < withdrawAmount) {
throw new Error('Insufficient funds');
}
// Continue with transfer...
});
How SecurityChecks Detects TOCTOU
Our static analyzer identifies:
- Check-then-act patterns - Conditional logic followed by state modification
- Non-atomic balance operations - Separate read and write without locking
- Missing transaction boundaries - Multi-step operations without transactions
- Optimistic lock bypass - Updates that don't verify version numbers
Testing for Race Conditions
Race conditions are notoriously hard to test. Here are some strategies:
it('should prevent double-spending via race condition', async () => {
const card = await createGiftCard({ balance: 100 });
// Fire 10 concurrent requests
const promises = Array(10).fill(null).map(() =>
redeemGiftCard(card.id, 100, userId).catch(e => e)
);
const results = await Promise.all(promises);
// Only one should succeed
const successes = results.filter(r => !(r instanceof Error));
expect(successes).toHaveLength(1);
// Balance should be 0, not negative
const updatedCard = await GiftCard.findById(card.id);
expect(updatedCard.balance).toBe(0);
});
Conclusion
TOCTOU vulnerabilities are subtle but can have severe consequences in financial systems. The key defenses are:
- Atomic operations - Combine check and action in a single operation
- Database locking - Use transactions with row-level locks
- Optimistic concurrency - Use version numbers to detect conflicts
- Idempotency - Design operations to be safely repeatable
SecurityChecks.ai helps you catch these issues in code review before they become production incidents.