The Gift Card Paid Twice — JavaScript Bug Hunt
Modelled on Egor Homakov's 2015 Starbucks report: he found that transferring balance between Starbucks gift cards was vulnerable to a race condition — by…
- Language: JavaScript
- Layer: Database
- Difficulty: Hard
- Concepts: Concurrency, Money
- Modelled on: Starbucks · 2015
- Visible tests: a single transfer moves the money; two concurrent full-balance transfers pay out once
- Reward: 50 XP for a complete fix
Briefing
Modelled on Egor Homakov's 2015 Starbucks report: he found that transferring balance between Starbucks gift cards was vulnerable to a race condition — by sending the same transfer request several times at once, the balance was credited more than once while the source card was debited only once, creating money that never existed.
transfer.js builds a transfer as a sequence of steps; scheduler.js runs steps from several transfers in a chosen interleaving, each step atomically. The transfer reads the balance in one step, credits in the next and writes the debit back in a third — so two transfers can both spend the same balance.
Fix makeTransfer so the balance check and the debit happen together, before anything is credited.
Bug report
BUG-SBUX-0515 · Priority: Critical (money creation) · Reported by: responsible disclosure
makeTransfer(store, from, to, amount) returns { steps, pc: 0, result: null }. scheduler.run(transfers, order) runs each step atomically, in any interleaving.
Whatever the interleaving:
- a transfer succeeds (result "ok") only if the source card held at least
amountat the moment it was debited; the destination is credited exactlyamount - otherwise result is "insufficient" and neither card changes
- money is conserved: the sum of all balances never changes
- a card's balance never goes below zero
store.debitIfAtLeast(card, amount) is an atomic check-and-debit.
Observed: two simultaneous transfers of a card's full balance both report "ok" and the destination receives it twice.
Logs
[cards] transfer 7713->2204 amount=10.00 ok
[cards] transfer 7713->2204 amount=10.00 ok
[ledger] reconciliation: card 2204 balance 20.00 exceeds funded total 10.00The code as shipped
src/cards/transfer.js (editable)
// Moves `amount` from one gift card to another as a sequence of steps.
exports.makeTransfer = function (store, from, to, amount) {
var t = { steps: [], pc: 0, result: null };
var balance = 0;
t.steps.push(function () {
balance = store.get(from);
});
t.steps.push(function () {
if (balance < amount) {
t.result = "insufficient";
return;
}
store.set(to, store.get(to) + amount);
});
t.steps.push(function () {
store.set(from, balance - amount);
t.result = "ok";
});
return t;
};
Read-only context: src/cards/scheduler.js, src/cards/store.js.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More JavaScript bug hunts.