Lion Cipher Lab
Exploit and Fix a Race Condition in Your Own Lab
- AppSec / DevSecOps
- Intermediate
- ~50 min
- Free · no account required
Why it matters
A race condition is one of the most counter-intuitive bugs in software: the code looks obviously correct when you read it top to bottom, and it passes every test you run one request at a time — and then it hands out unlimited money the moment two requests arrive together. The classic form is TOCTOU (time-of-check-to-time-of-use): the app READS some state ('is this coupon unused?'), does a little work, then WRITES ('mark it used, credit the wallet') — and in the gap between the check and the write, other requests read the same not-yet-updated state and all decide they may proceed. The result…
What you'll learn
- explaining a RACE CONDITION in plain words: when two or more requests are handled at the same time and step on each other's shared state because there is no lock or atomic operation to keep them from overlapping
- recognising the TOCTOU (time-of-check-to-time-of-use) 'check-then-act' shape — read a value, do some work, then write — and seeing why the gap between the check and the write is the whole vulnerability
- building a tiny vulnerable Flask + SQLite app on your own machine that redeems a single-use coupon by reading state, doing work, then writing, with no locking
- proving that a SINGLE sequential client behaves perfectly (the coupon redeems exactly once) so you understand why the bug is invisible to normal, one-request-at-a-time testing
- exploiting the race by firing many concurrent requests at once with a short Python threading script (or a seq | xargs curl one-liner on macOS/Linux) and watching several requests pass the check before any of them writes
- observing a LIMIT OVERRUN: the single-use coupon pays out ~30 times and the wallet holds $300 instead of $10 — the broken final state you created with your own attack
- fixing it the real way with a single ATOMIC conditional UPDATE (UPDATE coupon SET used=1 WHERE code=? AND used=0) and crediting only when cursor.rowcount == 1, then re-running the SAME attack to prove the balance now stays exactly $10
- knowing the judgment: the correct fix REMOVES the check-then-act gap (atomic conditional UPDATE, SELECT ... FOR UPDATE row locking, a database UNIQUE constraint, or an application mutex) — narrowing the window by deleting the sleep is NOT a fix, because single-packet / last-byte-sync attacks make even tiny windows exploitable over a network
- knowing where these bugs bite in the real world (payments, coupon and gift-card redemption, MFA/OTP verification, rate limits, file uploads) and the difference between a limit-overrun race on one endpoint and a multi-endpoint race
Steps in this lab
- The vocabulary: race conditions, TOCTOU, and the check-then-act gap
- Set up: confirm Python, make a folder, install Flask
- Build the VULNERABLE app (check, work, then write — no locking)
- Prove it is 'correct' one request at a time
- Build the attack: fire many requests at once
- Run the exploit and watch the limit overrun
- Fix it: collapse the check and the act into one atomic UPDATE
- Re-run the SAME attack and prove it is closed
- Judgment: the real remedies, and why 'make the window smaller' is not one
- What you did — and the reflex to keep