hunt-race-condition — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hunt-race-condition (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Race conditions are high-severity findings because they break financial, access control, and integrity assumptions that defenders rarely stress-test. Highest payouts come from:
Best-paying asset types: Fintech apps, SaaS platforms with credit/subscription models, social platforms with reputation systems, e-commerce checkout flows, OAuth/SSO token endpoints.
/vote, /upvote, /like, /favorite
/redeem, /apply-coupon, /use-code, /claim
/purchase, /checkout, /confirm-order, /pay
/transfer, /withdraw, /send-money
/invite, /referral, /accept-invite
/upgrade, /activate, /trial
/delete, /deactivate, /cancel
/follow, /subscribeX-RateLimit-* # rate limiting exists, but may not be atomic
X-Request-Id # each request independently tracked
No Cache-Control # stateful ops not idempotent// Single-use action buttons with client-side disable
button.disabled = true
$('#btn').prop('disabled', true)
// Optimistic UI updates (state set before server confirms)
setState({ used: true })
// Sequential async calls without locking
await useVoucher(); await deductBalance();with_lock / lock! — ActiveRecord doesn't lock by defaultSELECT ... FOR UPDATE — common in legacy codebasesINCR atomicity checksengine=Engine.BURP2 for last-byte synccurl with & backgroundingthreading or asyncio with pre-built connections200 OK where only one should succeed# turbo_intruder_race.py
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP2) # HTTP/2 single-packet
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
if '200' in req.status:
table.add(req)# Fire 15 simultaneous vote/redeem requests
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST "https://target.com/api/vote" \
-H "Cookie: session=YOUR_SESSION" \
-H "Content-Type: application/json" \
-d '{"report_id": "12345", "vote": "up"}' &
done
waitimport asyncio, aiohttp
async def race_request(session, url, payload, headers):
async with session.post(url, json=payload, headers=headers) as r:
return await r.text()
async def main():
url = "https://target.com/redeem"
payload = {"code": "GIFT50"}
headers = {"Cookie": "session=XXXXX"}
async with aiohttp.ClientSession() as session:
tasks = [race_request(session, url, payload, headers) for _ in range(20)]
results = await asyncio.gather(*tasks)
for r in results:
print(r[:100]) # print first 100 chars of each response
asyncio.run(main())# Look for read-then-write without locking
grep -rn "find_by\|where.*first" --include="*.rb" | grep -v "lock"
grep -rn "SELECT.*WHERE" --include="*.php" | grep -v "FOR UPDATE"
# JavaScript async without atomicity
grep -rn "await.*get\|await.*find" --include="*.js" -A2 | grep "await.*update\|await.*save"
# Python Django ORM without select_for_update
grep -rn "\.get(\|\.filter(" --include="*.py" | grep -v "select_for_update"# Verify target supports HTTP/2 (prerequisite for single-packet attack)
curl -sI --http2 https://target.com | grep -i "HTTP/2\|h2"if voucher.used == false), then writes state (voucher.update(used: true)) in two separate database operations. Any thread can read the same "unused" state before either writes.find or filter instead of SELECT ... FOR UPDATE. The fix is one line but developers don't think about concurrency.votes_count += 1; save() instead of an atomic SQL UPDATE SET votes = votes + 1 WHERE id = ?.false during a cache miss window when the first write hasn't propagated yet.Defense: Per-user rate limiting
Defense: Idempotency keys / unique request tokens
Defense: Database unique constraints
Defense: Short time windows / expiring tokens
Defense: Queue-based serialization
Defense: Application-layer mutex / locks
Defense: "Already used" checks in application code
UPDATE ... WHERE used=false RETURNING id truly prevents this.Before writing the report, confirm all three:
Can you demonstrate — with screenshots or logs — that the same one-time action succeeded more than once? (e.g., vote count shows +2 from one user, credit balance shows double-credit, coupon shows redeemed twice)
Is there concrete, measurable harm? Financial loss (credits issued in excess), integrity loss (manipulated rankings/votes), or security loss (access granted beyond entitlement)? "The counter went up twice" is only valid if that counter has real-world value.
Can you write a 20-line script, run it against a fresh test account, and reliably demonstrate the duplicate effect at least 3/5 attempts? If it requires perfect timing you cannot reliably control, the exploitability claim is weak.
A bug bounty platform's "popular reports" feature allowed upvotes to improve report visibility and researcher reputation scores. By sending ~15 parallel upvote requests for the same report using a single HTTP/2 connection (single-packet attack), a researcher was able to register 10–15 votes from a single account. This allowed artificial inflation of report rankings, manipulation of researcher reputation scores, and distortion of the platform's crowdsourced prioritization system — directly undermining trust in the platform's core feature for triaging vulnerability reports.
On a major social network (Facebook-scale), promotional or limited-use actions — such as adding a phone number for a one-time security credit, or claiming a one-time bonus — were vulnerable to simultaneous parallel requests. An attacker could race the claim endpoint and receive the promotional benefit multiple times, causing direct financial loss to the platform and allowing fraudulent accumulation of platform currency or benefits at scale. Given the user volume, even a brief window before patching represented significant financial exposure.
A cloud hosting provider enforced limits on the number of resources (e.g., droplets, projects, or API keys) a free-tier user could create. The limit check and resource creation were non-atomic operations. By racing the creation endpoint with 20 simultaneous requests, an attacker bypassed the enforcement logic and created resources far exceeding their tier limit. This translated directly to unauthorized compute consumption, billing fraud, and abuse of infrastructure — impacting both the provider's revenue and system stability for legitimate users.
The following real, verified bug-bounty / coordinated-disclosure cases extend this skill. Four cases (#4, #11, #12, plus the bonus reference) use the modern HTTP/2 single-packet attack technique (Kettle DEF CON 31, 2023; Flatt Security expansion 2024) — the technique that makes most modern race exploits viable today.
POST /-/profile requests changing email to two different addresses; the verification token sent to address A becomes valid for address B because state transitions weren't atomiccanVerifyForAction appended to an array without DB-level locking; fix added nullifiers table with atomic UPSERTpromotion_code.times_redeemedPOST /gift_cards/redeem → duplicate N× → fire parallel → balance credited N× from a single cardSELECT…FOR UPDATE around the redemption read/faucet/transfer requests; the Transfer Go function executes two state-mutating actions per request, both non-atomictoken_used flag committed → reward granted on every winning requestPOST /verify-pin requests in 166 ms, each with a different 4-6 digit guess, all landing inside the rate-limit windowPOST /checkout/PlaceOrder requests both applying the same gift card → both orders complete, gift card balance debited onceThe single-packet attack is the most important race-condition technique published since 2020. It collapses the race window from "tens of milliseconds with TCP-handshake jitter" to "the time the server's worker pool takes to dispatch N pre-buffered requests" — typically under 1 ms for the entire batch. This is what makes modern race exploits viable against rate-limited, distributed, load-balanced backends that previously seemed un-race-able.
Original research: James Kettle, PortSwigger — "Smashing the State Machine" (DEF CON 31, August 2023) portswigger.net/research/smashing-the-state-machine. 2024 extension: RyotaK / Flatt Security — "Beyond the Limit: Expanding Single-Packet Race Condition with First Sequence Sync" flatt.tech/research/posts/beyond-the-limit-expanding-single-packet-race-condition-with-first-sequence-sync/.
A race exploit fails for two reasons that look like the same problem but aren't:
The single-packet attack solves (1) by exploiting two protocol-level facts about HTTP/2:
HEADERS frames, and each HEADERS frame can be the last frame of a separate stream.So if you pre-stage N requests on a single HTTP/2 connection — send all the HEADERS frames except the very last byte of each, then release all the final bytes in a single TCP write — the TCP stack ships them in one IP packet (assuming < MTU, ~1500 bytes). The server's kernel hands all N requests to the HTTP/2 parser in the same scheduler tick. The race window is no longer the network — it's the application's own atomicity-failure window.
For (2) — server-side dispatch ordering — Kettle showed that modern backends (Node.js, Go, async Python) dispatch concurrently within microseconds when handed a packet of N pre-parsed requests. Older blocking backends (default Apache prefork, single-threaded PHP-FPM) serialise even with single-packet delivery; for those, the technique helps less but still wins over TCP-stream sequencing.
The exact mechanic Kettle documented:
HEADERS frame with the END_HEADERS flag and a DATA frame containing all but the last byte of the body. Do NOT set END_STREAM yet.The race window equals the time between worker N's SELECT ... FOR UPDATE and worker N+1's same query — typically nanoseconds when the workers run on the same CPU.
To confirm your attack tool is genuinely producing one-packet sync (vs accidentally fragmenting):
sudo tcpdump -i lo0 -w race.pcap port 443 (or interface 0).tls and tcp.port == 443.The Turbo Intruder engine=Engine.BURP2 implementation guarantees single-packet delivery on HTTP/2 targets when the request body fits in MTU. For larger bodies, see the "Race-window estimation" subsection below.
Two variants depending on what protocol the target speaks:
h2 in ALPN. Default approach.Content-Length confuses the front-end into emitting two HTTP/1.1 requests to the back-end on the same connection. Pairs with HTTP request smuggling (see hunt-http-smuggling). Useful when single-packet HTTP/2 is filtered at the front-end but the back-end is reachable in HTTP/1.1.Detect h2.0 viability via curl -sI --http2 https://target.com | grep -i HTTP/2. If the server doesn't speak h2, single-packet is not directly applicable — fall back to "parallel-pipelining" over HTTP/1.1 (much wider race window; usually loses the race against modern backends, but still useful for naive ones).
Before firing the attack, estimate the race window. This determines whether you need single-packet at all, and how many concurrent requests to send.
T_single.T_seq1, T_seq2.T_par1, T_par2.T_par1 ≈ T_par2 ≈ T_single, the server handles both in parallel — race window is min(T_par1, T_par2), single-packet helps a lot.T_par2 ≈ T_par1 + T_single, the server serialises — race window is whatever happens between sequential workers; single-packet helps less but still wins over TCP jitter.T_single to be 10–100 ms (DB query latency). The race window inside the server is typically < 1 ms (the gap between SELECT and UPDATE on the same row).N = 30 concurrent requests for single-packet h2. Increase to 100+ if the target's T_single is < 10 ms (very fast endpoint = larger pre-buffer needed to overflow the worker pool). Up to 10,000 with Flatt's first-sequence-sync extension (see below).A decision tree for picking the right shape:
Engine.BURP2 template — explaineddef queueRequests(target, wordlists):
# 1. Engine.BURP2 = HTTP/2 single-packet engine; provides the last-byte-sync primitive.
engine = RequestEngine(
endpoint=target.endpoint,
concurrentConnections=1, # 2. One TCP connection, multiplexing all streams.
requestsPerConnection=100, # 3. Up to 100 concurrent H2 streams. >30 needs Flatt-extension.
engine=Engine.BURP2, # 4. THE critical line — selects the single-packet engine.
pipeline=False, # 5. Pipelining is for HTTP/1.1; irrelevant on H2.
)
# 6. Build N requests. Each is identical here — racing the same endpoint.
# For PIN brute-force, vary the body across requests.
for i in range(30):
engine.queue(target.req)
# 7. openGate(...).complete(...) is the API call that performs last-byte-sync:
# - Buffer all 30 requests up to "last byte not sent"
# - Release all final bytes in a single TCP write
# - openGate returns immediately; complete waits for all responses.
engine.openGate("race1")
engine.complete(timeout=10)The Engine.BURP2 import does the heavy lifting. Behind the scenes:
engine.queue(req) adds a HEADERS frame to the connection's send buffer but withholds the last DATA frame byte.openGate("race1") blocks until all 30 are buffered, then issues a single socket.send(...) containing 30 × 1-byte DATA frames with END_STREAM. All 30 cross the wire in one IP packet (assuming < MTU).complete(timeout=10) collects responses and times.Inspect each response object: req.code, req.length, req.time. The race is "won" when at least 2 requests return a success that should logically have been mutually exclusive (e.g., both coupon-applies succeed when the redemption limit was 1).
Kettle's original single-packet caps at roughly N=30 due to MTU + TLS record limits. Flatt Security's RyotaK published the extension in August 2024:
Use case: brute-forcing 6-digit PINs (max 10^6 candidates) inside a 5-attempts-per-window cap. Without first-sequence-sync, you'd need ~200,000 windows. With it, ~100 windows.
Implementation: flatt.tech/research/posts/beyond-the-limit-... includes a working PoC.
| Scenario | Tool / variant |
|---|---|
Modern HTTPS target, ALPN advertises h2, body < 1400 bytes, need N ≤ 30 | Turbo Intruder Engine.BURP2 single-packet — default |
| Same as above but body > MTU | Multi-connection HTTP/2; widen window estimate by ~5 ms |
| Target speaks HTTP/1.1 only (no h2 ALPN) | curl --next parallel pipeline; race window is wide; only viable on slow servers |
| Need N > 30 (PIN brute-force, OTP exhaustion within rate-limit window) | Flatt first-sequence-sync extension; manual implementation per the writeup |
| Front-end h2, back-end h1 (CDN+origin) | h2.cl smuggling variant — pairs with hunt-http-smuggling |
| Quick reproducibility test on a single endpoint | curl --next --next --next --next (4-shot parallel HTTP/1.1) — wide window but no setup |
hunt-http-smuggling — h2.cl multi-frame varianthunt-mfa-bypass — OTP rate-limit window single-packet bypass (Flatt PIN-bruteforce class)hunt-business-logic — coupon / wallet / promo state-machine races where single-packet is the enabling primitivecurl --next parallel multi-request patterns.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.