Race Conditions in APIs: TOCTOU in Payments, Coupons, and Rate Limiting
Every standard scanner sends request A, waits for the response, and only then sends request B. Every race condition in production lives in the window between a server-side check and the update that follows it. These two facts guarantee that the most financially damaging class of bugs in APIs never appears in automated scan reports. The class: TOCTOU exploits in payment, coupon, and rate limiting endpoints.
The window exists between requests, not inside them. Until the testing tool changes how it delivers requests, the report will keep saying no issues found.
Sequential Scanners Cannot See the Time Window Between Requests
The check-then-act pattern looks atomic in code: SELECT balance WHERE id=X, verify, UPDATE balance. In concurrent execution, it creates a race window between those 2 database operations. If 2 requests hit the server in the same millisecond, both pass the check before either executes the update.
The OWASP API Security Top 10 (2023) classifies TOCTOU under A04 (Business Logic). No mainstream DAST tool tests this pattern by default because detection requires concurrent delivery, not sequential probing. The scanner sends the request, waits for the response, and moves on. The bug lives in exactly that wait interval.
CVE-2024-50379 (Apache Tomcat 9.x, 10.x, and 11.x, CVSS 9.8) is the severity ceiling for this class. Concurrent JSP requests raced between the file existence check and the write. The pattern is identical to payment APIs: read-check-write with no lock between operations. That this same pattern reaches CVSS 9.8 in widely deployed infrastructure confirms the class is not theoretical.
A clean scan report is not evidence of security. It is evidence of a tool that sends one request at a time.
HTTP/2 Single-Packet: 20-30 Simultaneous Requests in 1 TCP Packet
James Kettle (PortSwigger, Black Hat USA 2023, DEF CON 31) formalized the single-packet attack. The technique packs 20 to 30 HTTP/2 requests inside 1 TCP segment, eliminating network jitter as a variable. The result is an execution window below 1ms, compared to 4ms for the previous last-byte sync method.
HTTP/2 multiplexes streams over a single TCP connection. When all final frames arrive in the same packet, the kernel demultiplexes them to concurrent threads simultaneously. There is no arrival jitter because there are no separate arrivals.
The technique has 3 phases. In the first, N requests are sent with their final data frames held back. In the second, the tool waits for the in-flight propagation window. In the third, all final frames are released at once and the operating system groups them into 1 TCP packet. Kettle tested this over a 17,000km route (Melbourne to Dublin) and confirmed that exploitation no longer depends on geographic proximity.
For targets that do not negotiate HTTP/2, the fallback is last-byte sync. Send headers and body for all requests while holding the last byte, then release all final bytes simultaneously. Jitter is not eliminated, but it drops to microseconds. Larger batches compensate for the residual variance.
In Turbo Intruder, the configuration for single-packet is Engine.BURP2 with concurrentConnections=1:
def queueRequests(target, wordlists):
engine = RequestEngine(target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2)
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
For HTTP/1.1 with last-byte sync, use Engine.THREADED and increase the batch size to compensate for jitter.
Payment and Coupon Endpoints: P1 Bounties Because Financial Damage Is Quantifiable
TOCTOU in payment and coupon endpoints produces direct, measurable financial damage. The attacker controls the exact monetary outcome with the number of concurrent requests. That quantifiability is why HackerOne programs triage these reports as P1 or Critical.
H1 #429026 (HackerOne platform): race condition in retest payment confirmation. Simultaneous confirmation requests triggered multiple payments to the same researcher. Bounty: $2,500. The number of payments scales with the number of concurrent requests.
H1 #157996 (Instacart): the same coupon was redeemable multiple times before the system marked it as used. The marking happened asynchronously, after all concurrent requests had already passed the validation check. The accumulated savings scaled with request volume.
H1 #1717650 (Stripe): promotion codes exceeded the redemption_count limit per code via concurrent requests. Exploitable with browser tools, no specialized setup required. Direct impact on core payment infrastructure.
CVE-2026-31824 (Sylius e-commerce, CVSS 8.2) is the most structurally detailed case. The eligibility check reads the Doctrine ORM entity counter in memory during validation. The actual increment happens at order completion, with no database row lock between the 2 moments. Exploitation requires no authentication and allows unlimited redemption of coupons with defined limits. Affects all Sylius versions prior to 2.2.3.
The root cause repeats in any ORM without an explicit lock. Rails ActiveRecord, Django ORM, Mongoose, and Hibernate all exhibit this behavior by default. Protection requires an explicit SELECT FOR UPDATE. Alternatively, use an atomic increment with a WHERE clause that fails silently when the condition is no longer true.
Rate Limiting and Referral: Counter Reads Outside Transactions
Rate limiting and referral credit systems built on non-atomic counters share the same TOCTOU structure as payment endpoints. The difference is in the type of resource compromised: access vs credit vs money. The exploitation mechanics are the same.
H1 #759247 (Reverb.com): race condition in coupon and referral code redemption. Each concurrent request passed the "not yet used" check before any write marked the code as consumed. The pattern generalizes to single-use tokens, referral credits, and rate limit counters.
The classic rate limiting anti-pattern runs 3 separate operations: GET the counter value, compare it against the limit in application code, then increment. Each adjacent pair of operations creates a race window. Two concurrent requests read the same counter value, both pass the comparison, and both execute the increment.
Redis INCR is atomic. The problem appears when business logic involves a separate GET followed by comparison and SET. The comparison step falls outside Redis atomicity. The correct solution uses Lua scripts or Redis transactions to treat read, compare, and write as an indivisible operation.
For referral systems, the code validity check and the mark-as-used step are 2 separate database operations in ORM implementations. Concurrent checkout requests pass the validity check before any write completes.
Detection Methodology: Single-Packet, Timing, and Differential Confirmation
Reproducing race conditions requires synchronized delivery, not just speed. The methodology has 5 steps. Distinguishing a false negative from a non-vulnerable endpoint is as critical as finding the original bug.
Step 1: Identify endpoints with the check-then-act pattern. Primary candidates: balance deductions, coupon validation, rate limit increments, single-use token consumption, inter-account transfers. Every endpoint that reads state before modifying it is a candidate.
Step 2: Burp Repeater, "Send group in parallel" option with HTTP/2 active. Send 20 to 30 identical requests. A vulnerable endpoint returns multiple 200 responses where the system should accept only 1.
Step 3: Turbo Intruder with Engine.BURP2 and concurrentConnections=1 for single-packet mode. On a vulnerable target, all successful responses arrive within less than 10ms of each other.
Step 4: HTTP/1.1 fallback with last-byte sync when the target does not negotiate HTTP/2. Expect higher timing variance. Increase the batch and retry before dismissing the candidate.
Step 5: Distinguish false negatives. A single 4xx on retry means jitter collapsed the window, not that the endpoint is safe. Retry with more precise delivery and a larger batch before marking as non-vulnerable. Absence of reproduction is not proof of correction.
The (MAGO team tool) includes a race condition probe that uses HTTP/2 single-packet to test payment and redemption endpoints autonomously. The probe identifies candidates by differential response pattern and confirms with timing.
Race conditions leaving zero findings in a report is not evidence of absence. It is evidence of a tool that does not send concurrent requests. Every endpoint that reads a counter, checks a balance, or validates a single-use token before acting is a candidate. The single-packet attack made these exploits geography-independent in 2023. The bug class was not invented; it was made reliable.
Top comments (0)