In 2013, hackers exploited the gap between balance-check and balance-deduction (a TOCTOU race condition) in a prepaid-card processor's withdrawal logic, draining $45M by cashing out simultaneously from ATMs in 27 countries. I reproduced the same bug class and verified, by firing 10 concurrent withdrawal requests, that an atomic lock stops it.
A learning resource for backend developers who've written check-then-act logic against a shared resource like a balance — the gap between those two steps is exactly the vulnerability.
A vulnerable withdrawal API with a gap between the balance check and the deduction (TOCTOU).
// Vulnerable withdrawal API — modeled on the 2013 prepaid-card
// processor hack. The balance CHECK and the deduction ACT are two
// separate steps; the gap between them (a realistic DB round-trip)
// is a race window where concurrent requests all see the balance
// from BEFORE any of them has written back.
class RaceConditionWithdrawal {
constructor(account) {
this.account = account; // { balance }
}
async withdraw(amount) {
const current = this.account.balance; // CHECK
await simulatedDbRoundTrip(); // the race window
if (current < amount) return { status: 'insufficient_funds' };
this.account.balance = current - amount; // ACT — based on a stale read
return { status: 'ok', newBalance: this.account.balance };
}
}
module.exports = { RaceConditionWithdrawal };
The fix: the check and the deduction happen as one atomic, locked operation.
// The fix: no other request can act between the check and the
// deduction — an account-level lock makes both one atomic step.
// (A real system would use a DB row lock or an atomic
// UPDATE ... WHERE balance >= amount.)
class AtomicWithdrawalGuard {
constructor(account) {
this.account = account;
this.locked = false;
}
async withdraw(amount) {
while (this.locked) await sleep(5);
this.locked = true;
try {
await simulatedDbRoundTrip(); // same delay, now INSIDE the lock
if (this.account.balance < amount) return { status: 'insufficient_funds' };
this.account.balance -= amount;
return { status: 'ok', newBalance: this.account.balance };
} finally {
this.locked = false;
}
}
}
module.exports = { AtomicWithdrawalGuard };
Questions or feedback about this product? Leave your email if you'd like a reply (optional).