DEV Community

Rocky
Rocky

Posted on

The Race Condition Hiding Behind A Passing Manual Test

You're testing a checkout flow. There's a promo code field, one use per account, and the app enforces it correctly every time you try: submit the code once, get the discount, submit it again, get "code already used." You move on. It's not a finding, the control works.

Except you tested it the way a person naturally tests things: one request, wait for the response, then the next one. Nobody who built that feature, and nobody testing it by hand, ever actually sends two requests at the exact same moment. So nobody notices that "one use per account" isn't enforced by the database, it's enforced by hoping requests arrive one at a time.

Fire the same apply-code request twenty times concurrently, with something like Burp's Turbo Intruder instead of your browser, and a different picture shows up. The discount applies more than once. Sometimes twice, sometimes more, depending on how many requests land inside the window.

That window is the bug. The server logic is check-then-act: read whether the code is marked used, and if not, apply it, then mark it used. Each of those is fine as a single line of code. What's missing is anything that makes the read-then-write atomic across concurrent requests. When two requests both read "not used" before either one finishes writing "used," both apply the discount. This is a TOCTOU bug, time-of-check to time-of-use, and it has nothing to do with a typo. It's an unstated assumption, that requests arrive sequentially, that concurrency just breaks.

The reason this stays hidden from naive load testing too is timing. If your twenty requests leave your machine milliseconds apart, network jitter spreads them out enough that the server processes most of them sequentially anyway, and the race barely fires. The technique that makes it reliable is squeezing the requests into as few TCP packets as possible, so they arrive within microseconds of each other instead of milliseconds, using HTTP/2 multiplexing or a last-byte-sync trick to hold requests back and release them together. This is the single-packet attack technique documented in PortSwigger's published race condition research (James Kettle, "Smashing the State Machine"), and it's the difference between a race that wins once in fifty attempts and one that wins reliably enough to demonstrate as a finding.

Once you're looking for check-then-act gaps instead of just broken auth or missing validation, they show up everywhere a limited resource gets consumed: gift card redemption, withdrawal endpoints, "vote once" or "follow limit" features, even password reset tokens. Every one of them can look airtight under manual, sequential testing and fail the moment two requests land together.

That's the real shift this bug class asks for: from finding one broken control to understanding how controls interact under concurrency, which is business-logic thinking, not just OWASP Top 10 checklist thinking. It's the exact ground the Advanced Web Application Penetration Testing Book - L2 is built around, chained and logic-driven exploitation past the single-bug basics: https://resources.codelivly.com/product/web-application-hacking-l2/

The free Race Condition lab on codelivly.com is the hands-on version of exactly this bug, worth running before you go looking for it on a real target: https://codelivly.com/labs/race-condition

Top comments (0)