Race Condition Lab: Redeem a One-Time Coupon Twice

✨ AI Summary
DealStore's WELCOME10 coupon is good for $10 off, once. The code reads 'has this coupon been used?', sees no, applies the discount, then writes 'used'. Those are three separate steps — and if two requests arrive at the same instant, both can read 'no' before either writes 'used'.
Lab at a glance: Advanced · 100 XP · about 20 minutes · runs in your browser, no signup or setup. Try it yourself first; the full solution is behind a spoiler below.

What this lab teaches

A race condition happens when the result depends on the timing of events that were assumed to happen one at a time. The classic shape is check-then-act: check a condition, then act on it, with a gap in between where another request can slip in.

The lesson: a validity check and the update that enforces it must happen as one atomic operation. If they are two steps, concurrency can drive a truck through the gap.

How DealStore redeems a coupon

Redeeming WELCOME10 runs as: read whether the coupon is used, apply the $10 discount if not, then mark it used. Each step is a separate database touch, so two requests that arrive together can both pass the 'is it used?' check before either has written the 'used' flag.

The attack approach

  • Understand the redemption is check-then-act, with a window between the check and the write.
  • Prepare two (or more) identical redemption requests for the same order.
  • Send them as close to simultaneously as possible so both pass the 'unused' check.
  • Watch the discount stack beyond $10 as both requests apply it.

Full walkthrough (spoiler)

If sequential clicks only ever apply the coupon once, you need true concurrency — open the steps.

Show the step-by-step solution
  1. Set up two identical 'apply WELCOME10' requests against the same order.
  2. Fire them simultaneously (the lab provides a way to send concurrent requests) so both execute the check before either writes the used flag.
  3. Both requests read 'coupon not used', both apply the $10 discount, and only then does the used flag get written.
  4. The order's discount now exceeds $10. The lab detects the double discount and marks you solved.

Why it works

The code assumed redemptions happen one after another, so the gap between checking 'unused' and writing 'used' felt harmless. Under concurrency that gap is a window: multiple requests all see the stale 'unused' state and all act on it. Nothing is broken in any single request — the bug only appears when they overlap.

Core principleMake check-and-update atomic. Enforce single-use with a database constraint or a lock, not a read followed later by a write.

Where this happens in the real world

Check-then-act races drive real abuse: double-spending gift cards and coupons, withdrawing a balance more than once, and redeeming limited offers repeatedly. Concurrency bugs in payment and redemption flows are a steady source of bug-bounty reports, and they fall under Business Logic / Broken Access Control depending on what they bypass.

How to defend against it

  • Use a unique constraint (one redemption row per coupon per user) so the database rejects the second write.
  • Perform the check and the update in one atomic statement or inside a transaction with the right locking.
  • Use idempotency keys so repeated or concurrent requests collapse into a single effect.
  • Avoid read-then-write-later patterns for anything that must happen only once.
Mr Elite tip: To find races in your own app, take any 'once only' action — redeem, withdraw, vote, claim — and fire several identical requests at the same instant. If more than one succeeds, the check and the write are not atomic.

Try it, then go further

Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.

❓ Frequently asked questions

What is a race condition?
A bug where the outcome depends on the timing of concurrent operations. The classic case is check-then-act, where a gap between checking a condition and acting lets another request interfere.
Why does sending requests at once matter?
Fired together, multiple requests can all pass the same 'is it allowed?' check before any of them writes the result, so each one acts as if it were the only one.
How do I fix a check-then-act race?
Make the check and update atomic — use a unique database constraint, a transaction with proper locking, or an idempotency key.
Are race conditions only about money?
No, but money and limited offers are common targets. Any 'only once' action — voting, claiming, withdrawing — can be abused if the enforcement is not atomic.
⚡
Join free to earn XP for reading this article Track your progress, build streaks and compete on the leaderboard.
Join Free
Lokesh N. Singh aka Mr Elite
Lokesh N. Singh aka Mr Elite
Founder, Securityelites · AI Red Team Educator
Founder of Securityelites and creator of the SE-ARTCP credential. Working penetration tester focused on AI red team, prompt injection research, and LLM security education.
About Lokesh ->

Leave a Comment

Your email address will not be published. Required fields are marked *