Bypassing per-user purchase quotas, rate limits, and one-per-account promo restrictions via parameter tampering, race conditions, or UI-only enforcement.
TL;DR
Field(ge=1, le=N) validation + SELECT ... FOR UPDATE row lock + database UNIQUE constraint as the final safety net.Limit override is a class of business logic flaw where an attacker circumvents an application-defined quota — "max 2 per customer," "one promo per account," "5 free invites" — by exploiting a gap between the check that enforces the limit and the write that commits the state change. Classified under CWE-840 (Business Logic Errors) and surfaced by OWASP WSTG-BUSL-04 (Test for Limit Validation), it sits in the OWASP Top 10 2021 category A04:2021 — Insecure Design, because no amount of input sanitization or authentication will fix a missing transactional guarantee in the workflow itself.
The CVSS v3.1 base score is 6.5 (Medium) with vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N. While no canonical CVE has been published for limit override (the class is typically reported via private bug bounty programs rather than CVE-numbered disclosures), the closely-related CVE-2020-11007 (Shopizer negative quantity) and CVE-2023-45854 (Shopkit integer overflow) demonstrate equivalent quantity-bound bypass primitives in production e-commerce stacks.
The vulnerability is frequently confused with rate limiting (CWE-770), but the threat models are different. Rate limiting protects infrastructure from request floods (brute force, DoS, scraping) by throttling requests per IP or token over time. Limit override protects business resources — credits, redemptions, units purchased, referral payouts — by enforcing rules tied to user identity and product semantics. A rate limit bypass lets an attacker send 1,000 OTP requests per second; a limit override bypass lets the same attacker buy 1,000 units of a promotional item priced at $0.01. Both can co-exist on the same endpoint, but they are diagnosed and remediated independently.
The reason limit override is so common in modern stacks is architectural. Single-page applications enforce business rules in JavaScript (disabled buttons, hidden fields, client-side counters), while the underlying REST or GraphQL API often lacks the equivalent server-side guard. Microservice boundaries make atomic check-and-set hard, and HTTP/2 multiplexing lets an attacker land 50 requests within sub-millisecond windows — turning the gap into a reliable, repeatable exploit.
Applications impose limits on user behavior to protect revenue, prevent abuse, and ensure fair resource allocation. These limits fall into five categories, each with a typical enforcement location and a typical failure mode:
| Limit Type | Example | Common Enforcement Location |
|---|---|---|
| Purchase quantity | "Max 2 per customer" promotional item | Frontend form validation only |
| Usage quotas | Free tier: 100 API calls per day | Server-side counter in Redis or DB |
| Rate limits | 5 OTP attempts per account | Middleware counter on IP or session |
| Referral / promo caps | "Refer 5 friends max" per campaign | DB row check before insert |
| One-per-account promos | "First order discount, one use" | Boolean flag in user record |
The critical flaw pattern is the same across all five: the limit check happens in one layer (UI, middleware, or application code), but the final state-changing operation occurs in a different layer with no re-verification. The window between the check and the commit is the exploit surface, and it can be widened from microseconds to seconds by exploiting parameter tampering on the boundary value itself.
VULNERABLE PATTERN:
1. GET /api/limit-check -> returns {"allowed": true} <- check
2. POST /api/redeem-promo <- commit (no re-check)
SECURE PATTERN:
1. POST /api/redeem-promo -> BEGIN TRANSACTION
SELECT count(*) FROM redemptions WHERE user_id = ? FOR UPDATE
IF count >= max: ROLLBACK, return 429
INSERT redemption; COMMITThe secure pattern collapses check and commit into a single atomic database transaction with a row-level lock. Any concurrent request must wait for the first transaction to commit or roll back before its own SELECT FOR UPDATE can proceed, eliminating the TOCTOU window entirely. The vulnerable pattern, by contrast, allows N concurrent requests to each independently observe count = 0 before any of them increments the counter — and each subsequent INSERT succeeds because no unique constraint forbids it.
Six attack techniques cover the vast majority of limit override findings in production bug bounty reports:
| Variant | Technique | Impact |
|---|---|---|
| Negative quantity | Submit qty=-1 to subtract from cart total | Account credit / free items at checkout |
| Fractional quantity | Submit qty=0.6 for partial-price billing | Discount on every order (Zomato pattern) |
| Integer overflow | Submit qty=999999 to wrap arithmetic | Near-zero or negative total via overflow |
| Race condition | HTTP/2 single-packet, 20–50 parallel requests | Multiple redemptions of one-time promo |
| UI-only bypass | Replay successful action via Burp Repeater | Backend lacks the guard the UI enforces |
| Parameter forcing | Tamper state field (status=approved, tier=premium) | Workflow shortcut bypassing limit check |
Negative and fractional quantities exploit the absence of sign and type validation on numeric inputs. The server accepts the value, performs qty * unit_price, and either credits the user (negative result) or charges a discounted total (fractional result). Integer overflow targets typed-language back-ends where unchecked arithmetic on int32 wraps around: 2147483648 * 1 becomes -2147483648.
The most pervasive class is UI-only enforcement. Modern SPAs disable the "Apply Coupon" button after one use, hide the "Add Retailer" form after the third entry, or grey out the "Submit" button until a cooldown elapses. None of this enforcement crosses the network boundary. A single Burp Repeater replay of the original successful POST — with no UI involved — bypasses the entire check.
Race conditions exploit the gap between the application-level counter read and the database write. Using PortSwigger's HTTP/2 single-packet attack technique (last-byte synchronization, 2023), 20–50 identical requests arrive at the server within sub-millisecond windows. Each request independently passes the count < max check before any of them writes the new redemption row, and all of them succeed.
Zomato — HackerOne #403783 ($250, Medium): A researcher modified the support_rider_amount field to a negative decimal value. The server accepted the value and reduced the order total proportionally, effectively obtaining a discount on every order placed through the application. The validation layer checked that the field was numeric but never enforced a non-negative constraint or compared the field against any business-rule maximum. The fix required a positive-only validator on every monetary input field.
Upserve / OLO — HackerOne #364843 (Medium, undisclosed bounty): By including a menu item with quantity: -1 in the cart payload, the total price of the order was reduced. The negative quantity propagated unchallenged through the billing pipeline because the server validated that the item SKU existed but never validated the sign of the quantity. The exploit worked across all menu items, not just promotional ones, making it a category-wide checkout flaw rather than a single-item bug.
Curve — HackerOne #672487 (High, undisclosed bounty): Non-premium Curve accounts had a hard limit of 3 retailers eligible for the "Curve Cash" cashback feature. After reaching 3, the UI disabled the "Add retailer" button. A researcher captured the original POST /api/retailers/add request in Burp, replayed it via Repeater, and successfully added a 4th, 5th, and Nth retailer — each one earning cashback that should have been gated to premium tier. The backend had no equivalent server-side counter check; the entire enforcement was JavaScript-only.
HackerOne #115007 (race condition, invitation limit): A platform enforced a 3-invitation cap per account using an application-level counter read before the insert, with no row-level lock. By firing 7 concurrent invitation POSTs through Burp Intruder, a researcher successfully sent 7 invites — each one independently observing count < 3 before the others wrote their rows. The fix required a SELECT ... FOR UPDATE on the invitation count, plus a unique constraint on (user_id, invite_token).
UPchieve — HackerOne #1296597 ($100, Medium): A business logic error in the session-limit enforcement allowed students to consume more tutoring sessions than their account tier permitted. The check was performed at session-request time but not re-verified at session-start time, and a slow session-creation API allowed an attacker to queue multiple session requests before any of them completed.
Starbucks — Sakurity disclosure (2015, Critical, $500): A race condition in the gift card value-transfer flow allowed duplicate transfers. Two concurrent transfer requests from the same source card both passed the "sufficient balance?" check before either deducted the balance, effectively doubling the transferred value. Reported via responsible disclosure; patched within days. The case is widely cited because it demonstrates that even payment-grade code paths fail to use atomic transactions when the check and the write are split across services.
The PortSwigger and OWASP WSTG-BUSL-04 methodology for limit override testing follows three phases: enumeration of numeric parameters, parameter tampering, and concurrency testing.
Step 1 — Map all numeric parameters. Walk the application with Burp's proxy on, focusing on checkout, promo/coupon, referral, and subscription endpoints. Catalog every numeric field: quantity, amount, count, limit, uses, seats, credits, tier, discount. Note type, expected range, and whether the UI enforces the bound.
Step 2 — Tamper systematically with Burp Repeater. For each numeric parameter, send qty=0, qty=-1, qty=-9999, qty=0.1, qty=9999999, qty="1", qty="1abc", qty=null. Watch for response 200 with success body, total amount changing in attacker's favor, no 400/422 returned, or errors referencing arithmetic underflow/overflow.
Step 3 — Run race condition tests with Turbo Intruder or Burp Repeater Group. Capture a limit-bounded request and send it to Repeater. Use Burp's Repeater Group feature (2022+) with "Send group in parallel (single-packet attack)" enabled — this delivers 20–50 identical requests in a single TCP frame using HTTP/2 multiplexing. If 2 or more requests return success where only 1 should, TOCTOU is confirmed. Verify via GET /api/account afterward.
Generic web scanners typically miss limit override because the vulnerability requires application context: which endpoints have business limits, which parameters represent business resources, and what post-conditions to verify. A scanner that fires qty=-1 against every endpoint generates noise without confirmed findings — it has no way to verify the side effect without authenticating, navigating to the verification endpoint, and parsing application-specific JSON.
Effective automated signals: HTTP 200 returned on duplicate POST to a one-time-use endpoint, response body containing negative or zero monetary totals, discount_applied incrementing across identical requests, and divergence between direct API and UI-flow responses. Each requires a baseline and a comparison oracle.
BreachVex detects this through complementary techniques. One establishes a per-endpoint baseline with a legitimate request, then fires concurrent requests with TOCTOU-friendly timing and compares response bodies. Another walks the form catalogs and codes discovered during attack-surface mapping, identifies endpoints with one-time-use semantics (promo, invite, signup-bonus), and replays them with evidence capture to confirm the limit can be exceeded.
The check ("has the user used this promo?") and the write ("mark the promo as used") must be a single atomic database operation. The two-step pattern — SELECT then INSERT outside a transaction — is the root cause of nearly every TOCTOU race condition limit override.
Vulnerable Python (FastAPI + SQLAlchemy):
async def redeem_promo(user_id: str, promo_code: str, db: AsyncSession):
# Check
existing = await db.execute(
select(PromoRedemption)
.where(PromoRedemption.user_id == user_id)
.where(PromoRedemption.promo_code == promo_code)
)
if existing.scalar_one_or_none():
raise HTTPException(409, "Promo already redeemed")
# Commit (race window opens here — concurrent requests all pass the check)
db.add(PromoRedemption(user_id=user_id, promo_code=promo_code))
await db.commit()Secure Python — atomic transaction with SELECT FOR UPDATE:
async def redeem_promo(user_id: str, promo_code: str, db: AsyncSession):
async with db.begin():
# SELECT FOR UPDATE locks the row; concurrent transactions wait
result = await db.execute(
select(PromoRedemption)
.where(PromoRedemption.user_id == user_id)
.where(PromoRedemption.promo_code == promo_code)
.with_for_update()
)
if result.scalar_one_or_none():
raise HTTPException(409, "Promo already redeemed")
# Atomic insert; unique constraint catches any race survivor
db.add(PromoRedemption(user_id=user_id, promo_code=promo_code))
# COMMIT happens here; any duplicate raises IntegrityError -> 409Database constraint as final safety net (always add):
CREATE UNIQUE INDEX idx_promo_redemptions_user_promo
ON promo_redemptions (user_id, promo_code);The unique constraint is the last line of defense. Even if the application code is bypassed, the database refuses the duplicate insert at the storage layer with an IntegrityError, which the application catches and returns as 409 Conflict. This is defense in depth: application-level lock for normal flow, database-level constraint for race survivors and code-path bugs.
Never trust JavaScript-side limits as the only enforcement. Disabling a button, hiding a form, or counting clicks in localStorage is a UX feature, not a security control. The OWASP WSTG-BUSL-04 testing methodology explicitly tests for "client-side validation only" as a failure mode. Every business limit must be re-enforced server-side, in the same transaction as the state-changing write.
Every numeric input crossing the API boundary must be validated for sign, range, and type before any business logic runs. This stops the entire parameter-tampering family (negative, fractional, overflow) at the edge of the application.
Node.js (Zod + Express):
import { z } from "zod";
import type { Request, Response } from "express";
const AddToCartSchema = z.object({
product_id: z.string().uuid(),
// Reject negatives, fractionals, overflows, and string coercion
quantity: z
.number()
.int({ message: "Quantity must be a whole number" })
.min(1, { message: "Quantity must be at least 1" })
.max(100, { message: "Quantity cannot exceed 100 per order" }),
});
export async function addToCart(req: Request, res: Response) {
const parsed = AddToCartSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(422).json({ errors: parsed.error.format() });
}
const { product_id, quantity } = parsed.data;
// Safe: quantity is guaranteed integer in [1, 100]
await cartService.addItem(req.user.id, product_id, quantity);
return res.status(200).json({ ok: true });
}The z.number().int().min(1).max(100) chain rejects every parameter-tampering variant in one declaration: int() blocks 0.6 (Zomato), min(1) blocks -1 (Upserve) and 0, max(100) blocks 999999 (overflow). Combined with the atomic SELECT FOR UPDATE pattern and a unique database constraint, the application has three independent defense layers — all three must fail simultaneously for an exploit to succeed.
For payment and promo operations, also require an idempotency key in every mutation request. The server stores (user_id, idempotency_key) -> result in Redis with a 24-hour TTL and returns the cached result for duplicates. This neutralizes replay attacks at the protocol level. Stripe's idempotency-key design is the canonical reference.
qty=-1 to bypass minimum).Limit override is a class of business logic flaw where an attacker circumvents an application-defined quota — 'max 2 per customer', 'one promo per account', '5 free invites' — by exploiting a gap between the limit check and the state-write commit. Mapped to CWE-840 (Business Logic Errors), surfaced by OWASP WSTG-BUSL-04, and listed under OWASP A04:2021 (renamed A06:2025 — Insecure Design). Real cases: HackerOne #403783 Zomato, HackerOne #672487 Curve, HackerOne #364843 OLO.
Six techniques. (1) Negative quantity (qty=-1) inverts the consumption arithmetic. (2) Fractional quantity (qty=0.6) underconsumes the quota. (3) Integer overflow (qty=999999) wraps the counter. (4) HTTP/2 single-packet attack races N parallel requests past the limit check. (5) UI-only enforcement bypass via Burp Repeater (HackerOne #672487 Curve). (6) Parameter forcing on state fields (used_count=0). Real bounties range from $100 (UPchieve) to $25K+ for revenue-impacting bypasses.
Rate limiting (CWE-770 Allocation of Resources Without Limits) is an infrastructure control — limit requests per second per IP/user to prevent DoS. Limit override (CWE-840 Business Logic Errors) is the defeat of an application-defined business quota — '3 free trials per email', '5 invites per campaign', '$500 daily withdrawal cap'. Rate limiting is enforced at the middleware layer (Cloudflare, NGINX, SlowAPI). Business limits must be enforced at the application layer with atomic database transactions (SELECT FOR UPDATE), because they involve domain-specific state, not just request counts.
Because the UI is just one client of the API — disabling the 'Add' button after 3 entries does not stop a Burp Repeater request from hitting POST /api/retailers/add a fourth time. HackerOne #672487 (Curve, High severity) is the textbook case: a non-premium account had a UI-enforced 3-retailer limit; the researcher replayed the same POST request via Burp and added a 4th, 5th, and Nth retailer because the backend never verified the count. Every limit must be enforced server-side with an atomic check-and-write.
Three-layer defense. (1) Frontend: Zod or Pydantic schema with .max(1). (2) Application transaction: BEGIN; SELECT count(*) FROM redemptions WHERE user_id=? AND promo_id=? FOR UPDATE; if count >= 1 rollback; INSERT redemption; COMMIT. (3) Database constraint: UNIQUE INDEX redemptions_user_promo_idx ON (user_id, promo_id) so duplicate inserts fail at the schema layer even if application logic is bypassed. The combination defeats parameter tampering, race conditions, and direct database manipulation.
OWASP WSTG-BUSL-04 — Test for the Circumvention of Limit Validations is the OWASP Web Security Testing Guide methodology for business-quota bypass. It instructs testers to identify every quantitative limit in the application (purchase caps, rate limits, redemption counts, file size, withdrawal caps) and probe each with: zero values, negative values, fractional values, integer overflow, parallel requests, UI bypass, and parameter forcing. WSTG-BUSL-04 is part of the broader BUSL family covering all 10 OWASP business logic test cases.
Three signals. (1) Backend response shows count > UI-displayed maximum (e.g., 5 retailers attached when UI says max 3). (2) Replay the same POST after the UI button is disabled — successful 200 OK with new resource ID confirms backend has no enforcement. (3) Concurrent submission test: send 10 parallel POST requests via Turbo Intruder; if all 10 succeed, no atomic enforcement exists. BreachVex automates all three signals — baseline-vs-concurrent comparison plus one-time-use replay — so the same checks run without manual Burp orchestration.