Concurrent requests bypass usage limits (rate limits, stock checks, wallet balances) before the decrement is committed to the database.
TL;DR
POST /vote all see has_voted=false before any user_voted flag commitsUPDATE WHERE count < limit or INSERT ON CONFLICT DO NOTHING — never SELECT-then-UPDATEA limit override race condition exploits any system that enforces a numerical or boolean limit via a non-atomic check-and-increment sequence. The application checks that the current count is below the limit (SELECT), confirms the condition passes, then increments the counter (UPDATE) in a separate operation. Concurrent requests all pass the check before any increment commits — effectively bypassing the limit.
This is the broadest subcategory of web race conditions, covering: rate limits (N requests per minute), vote/like caps (1 vote per user per post), inventory limits (quantity in stock), subscription quotas (API calls per month), free trial limits (1 trial per email), and anti-automation controls (1 CAPTCHA per account creation). The pattern is uniform across all domains: read current state → check against limit → act → update state.
The James Kettle single-packet attack (Black Hat 2023) made limit override races reliably exploitable: before HTTP/2 last-byte sync, the 15–30ms network jitter between concurrent requests often caused the server to serialize them. With single-packet delivery, all 20 requests arrive in under 1ms and are genuinely processed concurrently by the backend.
All limit override races share the same non-atomic pattern:
1. READ: SELECT count FROM limits WHERE user=X AND type='vote' → count=0
2. CHECK: if count < max_votes (0 < 1 = TRUE)
3. ACT: Process the vote/action
4. UPDATE: UPDATE limits SET count = count + 1 WHERE user=X AND type='vote'Twenty concurrent requests all execute step 1 (READ count=0) before any execute step 4 (UPDATE to 1). All 20 pass step 2 and complete step 3.
# Vote inflation PoC — N concurrent votes from same user
import asyncio, httpx
async def vote_inflate(
vote_url: str,
post_id: str,
auth_headers: dict,
n: int = 50
):
"""
Endpoint: POST /api/posts/{id}/vote
Naive check: if not has_voted(user, post): increment vote_count
Race: N parallel calls all see has_voted=false before any flag sets.
"""
async with httpx.AsyncClient(http2=True, verify=False) as client:
await client.get(vote_url.rsplit("/", 2)[0] + "/") # Warm
tasks = [
client.post(vote_url.format(id=post_id), headers=auth_headers,
json={"value": 1})
for _ in range(n)
]
responses = await asyncio.gather(*tasks, return_exceptions=True)
successes = sum(1 for r in responses if not isinstance(r, Exception)
and r.status_code in (200, 201))
return {"votes_registered": successes, "race_confirmed": successes > 1}| Variant | Limit Bypassed | Impact | Real Example |
|---|---|---|---|
| Rate limit bypass | Requests per minute/second | Brute force enabled, CAPTCHA defeat | AWS WAF bypass (2024 bounty) |
| Vote/like inflation | 1 vote per user per post | Poll manipulation, app review bombing | Reddit-like platforms |
| Faucet/airdrop multi-claim | 1 claim per address per period | Free tokens × N | Cosmos starport ($5K) |
| Free trial extension | 1 trial per email | Unlimited free access | SaaS platforms |
| CAPTCHA token reuse | 1 solve per registration | Mass account creation | Signup anti-automation |
| Stock over-purchase | Available quantity | Negative inventory, overselling | E-commerce platforms |
| API quota bypass | Monthly API call limit | Exceed tier limits for free | API monetization platforms |
Rate limit bypass is the most technically significant variant. The Cloudflare and AWS WAF token-bucket rate limiters process the counter increment asynchronously — when 20 requests arrive in one TCP packet, all 20 are validated against the pre-increment counter and all pass. This was confirmed in 2024 bug bounty reports and acknowledged by both vendors.
Faucet multi-claim is the most financially impactful Web3 variant. The Cosmos/starport faucet awarded $5,000 for a race that allowed 30 concurrent claims from one address, bypassing the 1-claim-per-day limit. In a governance context, N faucet claims enable N voting shares — potentially enabling minority-to-majority governance manipulation on small DAO tokens.
CAPTCHA race enables mass account creation from a single CAPTCHA solve. If the token is validated (SELECT valid=true) and then marked used (UPDATE valid=false) in two steps, all N concurrent signup requests with the same CAPTCHA token pass validation before any marking commits.
Cosmos/starport Faucet Race (HackerOne, $5,000)
The Cosmos blockchain testnet faucet enforced "1 claim per address per 24 hours" via a SELECT-then-INSERT pattern: check if address has claimed today, then record the claim. A single wallet address sent 30 concurrent claim requests before any INSERT committed. All 30 passed the "has claimed today?" check and all received the daily allowance. Used to demonstrate faucet economics abuse: repeated 30× claims with $5K bounty awarded for the vulnerability.
Tools for Humanity (Worldcoin) Verification Race ($3,000)
A biometric verification check at Tools for Humanity allowed a race condition that bypassed a uniqueness constraint. The check ("has this biometric been registered?") and the registration step were non-atomic. Concurrent verification requests all passed the uniqueness check and all registered — generating multiple account credits from one scan.
InnoGames Email Activation Race ($2,000)
Twenty concurrent email activation requests each incremented a "activation reward" counter on a gaming platform, yielding 20× the welcome currency bonus. The activation endpoint lacked atomic counter enforcement — each request read rewarded=false before any write committed.
Helium Transfer Data Credits Race ($250)
The Helium blockchain's data credit transfer endpoint had a non-atomic balance check. A small concurrent burst transferred data credits N times, exceeding the available balance. Awarded $250 HackerOne bounty for a low-severity demonstration of the pattern.
AWS WAF Rate Limit Bypass (2024 Bug Bounty)
An undisclosed 2024 HackerOne report confirmed that HTTP/2 single-packet requests bypass AWS WAF's token-bucket rate limiter when all requests arrive in the same TCP packet. AWS acknowledged the limitation is architectural — the counter is evaluated per-connection before increment, so concurrent streams on one connection all read the pre-increment value. Mitigation: combine gateway rate limiting with application-level idempotency.
/vote, /follow, /like, /claim, /faucet, /free-trial, /register.For rate limit bypass specifically:
# Limit override detection — N-concurrent + state proof
async def limit_probe(limit_url: str, action_url: str, action_body: dict,
headers: dict, n: int = 20, expected_limit: int = 1):
"""
Send N concurrent requests to an endpoint with a per-user limit.
Compare success count against expected_limit.
"""
async with httpx.AsyncClient(http2=True, verify=False) as client:
# Check current usage
pre = (await client.get(limit_url, headers=headers)).json()
# Race N requests
tasks = [client.post(action_url, headers=headers, json=action_body)
for _ in range(n)]
results = await asyncio.gather(*tasks, return_exceptions=True)
successes = sum(1 for r in results
if not isinstance(r, Exception) and r.status_code in (200, 201))
# Check post-race state
post = (await client.get(limit_url, headers=headers)).json()
return {
"successes": successes,
"pre": pre,
"post": post,
"limit_bypassed": successes > expected_limit,
}BreachVex detects limit override races through complementary techniques: matching URL patterns of known limit-enforcing endpoints (/vote, /claim, /faucet, /follow, /register), firing concurrent bursts with response-differential analysis, and verifying state on the associated counter endpoint.
The universal fix — check and increment in one SQL statement:
-- VULNERABLE: SELECT then UPDATE — race window
SELECT vote_count FROM user_votes WHERE user_id = $1 AND post_id = $2;
-- (race window: concurrent reads all see no prior vote)
INSERT INTO user_votes (user_id, post_id) VALUES ($1, $2);
UPDATE posts SET vote_count = vote_count + 1 WHERE id = $2;
-- FIXED: atomic INSERT with conflict detection
INSERT INTO user_votes (user_id, post_id)
VALUES ($1, $2)
ON CONFLICT (user_id, post_id) DO NOTHING;
-- Only increment if the insert succeeded (returned 1 row)
-- If ON CONFLICT triggered (duplicate vote), do NOT increment# SQLAlchemy — atomic vote with conflict handling
from sqlalchemy import insert
from sqlalchemy.dialects.postgresql import insert as pg_insert
async def cast_vote(user_id: int, post_id: int, db: AsyncSession):
stmt = pg_insert(UserVote).values(user_id=user_id, post_id=post_id)
stmt = stmt.on_conflict_do_nothing(index_elements=['user_id', 'post_id'])
result = await db.execute(stmt)
if result.rowcount == 0:
raise ValueError("Already voted") # Conflict triggered — duplicate
# Only increment if insert succeeded
await db.execute(
update(Post)
.where(Post.id == post_id)
.values(vote_count=Post.vote_count + 1)
)
await db.commit()For numerical limits (stock, API quota, rate limit):
-- Atomic counter increment with maximum enforcement
UPDATE usage_counters
SET count = count + 1,
last_used_at = NOW()
WHERE user_id = $1
AND endpoint = $2
AND period_start = date_trunc('hour', NOW())
AND count < max_calls_per_hour
RETURNING count;
-- 0 rows = limit reached — no race possibleimport redis.asyncio as redis
async def rate_limit_atomic(user_id: int, endpoint: str, limit: int,
window_sec: int, r: redis.Redis) -> bool:
"""Atomic rate limit using Redis INCR — no race possible."""
key = f"ratelimit:{user_id}:{endpoint}:{window_sec // window_sec}"
# INCR is atomic — no race between check and increment
count = await r.incr(key)
if count == 1:
# First request in window — set expiry
await r.expire(key, window_sec)
if count > limit:
return False # Rate limit exceeded
return TrueApplication-level rate limits that use SELECT-then-UPDATE on a counter column are vulnerable to this exact attack. Redis INCR (atomic) and database atomic conditional UPDATE are the only safe patterns. A SELECT-then-UPDATE rate limiter provides zero protection against HTTP/2 concurrent bursts.
A limit override race condition bypasses any numerical limit enforced by a non-atomic read-then-write on a counter. The application reads the current count (SELECT), checks that count < limit, then increments (UPDATE). Concurrent requests all pass the check before any increment commits, effectively bypassing the limit. Common targets: rate limits, vote limits, stock quantity, free trial duration, API call quotas, and anti-automation counters.
Rate limiters increment a per-user counter after each request is processed. With HTTP/2 single-packet attack, all N requests arrive at the server before any request has been processed and any counter has been incremented. All N requests read count=0 and pass the rate check. This has been confirmed against Cloudflare R2 and AWS WAF token-bucket implementations in 2024 bug bounty reports. The fix requires atomic enforcement at the business logic layer, not just the gateway.
An atomic conditional UPDATE prevents vote manipulation: UPDATE posts SET votes = votes + 1 WHERE id = $1 AND NOT EXISTS (SELECT 1 FROM post_votes WHERE post_id = $1 AND user_id = $2). This checks and records the vote in a single atomic statement. The alternative is an INSERT INTO post_votes (post_id, user_id) ON CONFLICT DO NOTHING, then only incrementing the counter if the INSERT succeeded.
A crypto faucet with '1 claim per address per 24h' enforces uniqueness via a SELECT-then-INSERT: check if address claimed today, if not, INSERT a claim record and transfer tokens. Concurrent requests from the same address all pass the SELECT check before any INSERT commits. The Cosmos/starport faucet race ($5,000 HackerOne bounty) allowed 30 concurrent claims from a single address, each receiving the full daily allowance.
Yes. CAPTCHA validation typically marks a token as used after validating it (SELECT valid, then UPDATE used=true). The window between these two steps allows N concurrent requests with the same CAPTCHA token to all pass validation before the token is marked used. This enables creating multiple accounts from a single CAPTCHA solve, bypassing anti-automation controls designed to prevent mass account registration.
A rate limit bypass targets a temporal counter (N requests per minute/hour) that resets on a schedule. The race exploits the increment lag: all N single-packet requests arrive before the counter increments from 0, so all pass the count < limit check. A limit overrun targets a permanent or per-resource counter (1 vote per user per post, 1 trial per email) that never resets. Both share the same non-atomic check-increment pattern, but the temporal vs. permanent distinction affects the fix: rate limits can use Redis INCR (atomic, TTL-based); per-resource limits require INSERT ON CONFLICT DO NOTHING or atomic UPDATE WHERE count < max.
In token-based DAOs, voting power is proportional to token holdings. A faucet or airdrop with a per-address limit prevents any single address from accumulating disproportionate voting power. If the faucet has a limit-overrun race (SELECT-then-INSERT claim record), an attacker can claim 30× or 100× the intended allocation from a single address. With sufficient duplicated tokens, the attacker gains majority voting power — enabling malicious governance proposals: treasury drains, contract upgrades, or parameter changes. The Cosmos/starport $5,000 bounty was specifically flagged as a governance security risk beyond the direct token value.
An e-commerce platform checking inventory before processing an order (SELECT quantity FROM inventory WHERE sku=X; if quantity > 0: process_order()) is vulnerable to concurrent order submissions. Twenty concurrent order requests all read quantity=1 and all pass the check. All 20 orders are processed and fulfilled. The platform ships 20 units it has only 1 of — incurring 19 units of real-world financial loss either via refunds or by purchasing emergency stock at full price. Physical inventory with high unit value (electronics, limited-edition items) makes this commercially significant: a $500 GPU raced 20 times costs the platform $9,500 in losses from a single attack.
The standard HTTP/2 single-packet attack (Kettle 2023) works with 20–30 concurrent requests — sufficient for most limit races. First sequence sync (Flatt Security CODE BLUE 2024) enables up to 10,000 concurrent requests by exploiting IP fragmentation to synchronize connection establishment across all requests. For faucet/airdrop races, 30 concurrent claims may provide meaningful financial gain but 10,000 concurrent claims dramatically amplifies the attack. First sequence sync is most relevant when (1) the target has a per-period limit high enough that 30 concurrent bypasses are commercially insignificant, or (2) the attacker wants to drain a shared resource (token pool, credit pool) before legitimate users claim it. Requires Scapy-based raw packet access — not feasible from standard cloud infrastructure without root/CAP_NET_RAW.
Redis INCR is a single atomic server-side operation — the read, increment, and write happen in one Redis command with no intermediate state visible to other clients. No concurrent INCR call can read the pre-increment value because Redis processes commands sequentially in a single thread. SELECT-then-UPDATE in SQL are two round-trips with a visible intermediate state: any concurrent transaction can read the value between the SELECT and UPDATE. The database also has to respect its isolation level and may allow multiple readers before any writer commits. Redis INCR has no concept of 'between reads and writes' — it is instantaneous from every other client's perspective. This makes INCR the correct atomic building block for rate limiters, with EXPIRE called conditionally only when INCR returns 1 (first request in window).