BowBuy
A members-only archery shop hands every new face a welcome credit — generous, until you notice nothing on the racks costs less than it. See what you can carry out the door anyway.
The Scenario
BowBuy runs lean: a Blue Ridge pro shop that went online, a small crew, and a launch promo that drops a little store credit into every new member's account to get them started.
The catalog, though, is all premium glass and carbon — every rig priced just out of reach of that welcome handout. The crew are sure no new member walks out with a bow on day one. Prove them wrong, and the thank-you gift is yours.
Lab Intel
Synopsis
A premium archery store whose new-member '$300 welcome credit' is applied at checkout through a check-then-act race. The redeem endpoint reads a one-time flag, pauses to 'authorize', then adds the credit and sets the flag — so concurrent redeems all pass the check and each adds $300. Stack the credit past an item's price, place the order, and the confirmation page renders the gift token (the flag).
Architecture
An easy single-app lab: an nginx gateway in front of one Flask storefront run under gunicorn with a single worker and a thread pool (--workers 1 --threads 8) over SQLite (WAL). New accounts start with balance 0 and welcome_redeemed 0; every product is priced above the $300 welcome credit (cheapest item $329), so no single legitimate purchase clears. POST /checkout/redeem is the flaw: it CHECKS the one-time flag with a plain read (no lock held), pauses ~70 ms to 'authorize the credit' (releasing the GIL), then USES it — an atomic SQL increment balance = balance + 300 plus welcome_redeemed = 1. Because the check and the set are not atomic, ~10-15 concurrent redeems all pass the check inside the window and each adds $300, stacking the balance to $600/$900/$2400…. With balance >= price, POST /checkout/place succeeds (its debit is a single atomic UPDATE ... WHERE balance >= total, so it cannot be double-spent or driven negative) and creates a paid order; GET /order/<id>/confirmation — gated to the session user's own paid order — renders the flag as a 'gift with purchase'. Everything else is hardened so the race is the only path: all prices and the order total are computed server-side, the credit amount is server-fixed, quantity is clamped to a positive integer, and the confirmation page is IDOR-safe (id + owner + status checked in one query). Solve with Burp Turbo Intruder (or a ~15-thread script) firing many /checkout/redeem requests at once, then place the order.
Who It's For
Players meeting race conditions for the first time. You should be comfortable sending many requests concurrently (Burp Turbo Intruder, a threaded Python script) and reasoning about what happens when a check and its effect are not atomic.
Skills You'll Practice
- Identifying a check-then-act (TOCTOU) race in business logic
- Distinguishing logic that is safe sequentially but unsafe under concurrency
- Driving a race with a concurrent request tool and confirming the stacked state
- Recognising that a one-time guard must be atomic with the effect it protects
What You'll Gain
- A working mental model of TOCTOU / check-then-act races
- Why 'it's a one-time action' is not a guarantee without atomicity
- A reusable approach for racing one-time credits, coupons, and redemptions