Back to Blog
Security

Race Conditions in Payment Systems: TOCTOU Vulnerabilities

Time-of-check to time-of-use vulnerabilities can lead to double-spending and financial loss. Learn how to identify and prevent these subtle but critical bugs.

NP
Nina Patel
Platform Security Lead
December 20, 20244 min read
race conditionsTOCTOUpaymentsconcurrency

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:

  1. Check-then-act patterns - Conditional logic followed by state modification
  2. Non-atomic balance operations - Separate read and write without locking
  3. Missing transaction boundaries - Multi-step operations without transactions
  4. 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.

Enjoyed this article?

Subscribe to get the latest security insights delivered to your inbox.