Race Conditions in Billing Systems and Why They're Underreported
Of the three FOSSBilling bugs I found, this is the one I think gets missed most often across the industry, not because it's subtle, but because finding it requires thinking about the app in a dimension most testing simply doesn't cover: time.
What black-box scanners don't ask
Automated scanners send one request, look at one response, and move on. Manual testers, myself included on a rushed day, tend to think the same way: does this input get validated, does this action require auth, does this response leak something. Almost nobody's default question is "what happens if I send this exact same request twice, at the exact same instant?" Race conditions live entirely in that gap.
I started looking at FOSSBilling's promo code logic specifically because discount codes are a classic place for check-then-act mistakes, an application has to check that a code is still valid, then separately record that it's been used, and there's always a window between those two steps.
Two operations, no lock between them
The cart checkout flow: read the promo code's current usage count, compare it to the limit, and if it passes, increment the count and create the order. Three separate database operations. No transaction wrapping them, no row-level lock holding the count steady in between.
If two checkout requests for the same single-use code arrive close enough together, both read the counter before either one increments it. Both see the code as unused. Both pass the check. Both create a discounted order. The code was single-use in name only.
Building the window wide enough to walk through
Proving this needed more than "send it twice and hope." I used Python's threading.Barrier to hold a pool of worker threads at a starting line and release them in the same instant, so the requests actually land on the server concurrently instead of trickling in one after another:
import threading, requests
barrier = threading.Barrier(N)
def checkout():
barrier.wait() # hold every thread here, then release together
requests.post(checkout_url, json=order_payload)
threads = [threading.Thread(target=checkout) for _ in range(N)]
[t.start() for t in threads]
[t.join() for t in threads]
In testing, a single-use code cleared ten or more successful discounted checkouts from one burst. The fix on the vendor's side was straightforward once the race was demonstrated: wrap the check and the increment in one atomic operation instead of two independent ones.
Why I think this class stays underreported
A few reasons keep coming up whenever I compare notes with other researchers. Race conditions don't reproduce reliably with a single request, so they're invisible to anyone testing sequentially. They require the tester to model the application's state transitions, not just its inputs and outputs. And exploiting them well needs tooling most web testers don't reach for by default, precise concurrency, not just a for-loop. None of that is exotic, but it is a different mental model than "fuzz the parameter," and that gap is exactly why this bug class is where I want to keep digging, both in bug bounty work and, longer term, in research: the tooling and methodology for finding logic-timing bugs at scale is genuinely underdeveloped compared to what exists for memory safety or injection classes.