highCWE-362A04:2021

Race Condition in Payments

Read-modify-write payment operations without database transactions allow attackers to exploit timing windows and spend the same balance multiple times.

How It Works

A race condition in payments occurs when the application reads a user's balance, checks if it is sufficient, and then deducts the amount in separate non-atomic operations. If an attacker sends multiple simultaneous requests, all of them may read the original balance before any deduction is written. Each request sees sufficient funds and proceeds with the purchase. For example, a user with $100 sends 10 concurrent requests for a $50 item. All 10 read $100, all 10 approve the purchase, resulting in $500 in goods for $100. This is called a TOCTOU (Time Of Check to Time Of Use) vulnerability and is common in e-commerce, in-app currencies, and wallet systems.

Vulnerable Code
app.post('/api/purchase', async (req, res) => {
  const user = await db.user.findUnique({ where: { id: req.userId } });
  if (user.balance < req.body.amount) {
    return res.status(400).json({ error: 'Insufficient funds' });
  }
  await db.user.update({ where: { id: req.userId },
    data: { balance: user.balance - req.body.amount } });
  await db.order.create({ data: { userId: req.userId, amount: req.body.amount } });
  res.json({ success: true });
});
Secure Code
app.post('/api/purchase', async (req, res) => {
  const result = await db.$transaction(async (tx) => {
    const user = await tx.user.findUnique({ where: { id: req.userId } });
    if (user.balance < req.body.amount) throw new Error('Insufficient funds');
    const updated = await tx.user.update({ where: { id: req.userId },
      data: { balance: { decrement: req.body.amount } } });
    if (updated.balance < 0) throw new Error('Insufficient funds');
    return tx.order.create({ data: { userId: req.userId, amount: req.body.amount } });
  });
  res.json({ success: true });
});

Real-World Example

In 2018, Starbucks disclosed a race condition in their gift card system that allowed attackers to duplicate balances by making simultaneous transfer requests between cards. The bug was discovered through their bug bounty program and had been exploited in the wild to generate unlimited store credit.

How to Prevent It

  • Wrap all balance read-check-update operations in a database transaction
  • Use atomic operations like decrement instead of reading and subtracting
  • Add optimistic locking with version columns to detect concurrent modifications
  • Implement idempotency keys to prevent duplicate processing of the same request

Affected Technologies

Node.jsReactNext.jsPythonGoJavaPHP

Data Hogo detects this vulnerability automatically.

Scan Your Repo Free

Related Vulnerabilities