Every vulnerability Web Security's domain covered involves malformed input — a string that shouldn't be there, a query that shouldn't parse the way it does. A business logic vulnerability involves no malformed input at all — every single request is perfectly well-formed, and the application is still doing something it was never supposed to allow.
3 min read
A SQL injection scanner checks whether input breaks out of a query's intended structure. A business logic flaw involves no broken structure at all — every field is valid, every request is syntactically correct, and the application faithfully executes exactly what it was coded to do. The bug lives entirely in a gap between what the workflow was supposed to enforce and what the code actually checks — which is precisely why automated scanners (SAST/DAST, covered in lesson 5) are structurally unable to catch most of these: there's no malformed pattern to detect.
A checkout flow expects: add items → enter shipping → enter payment → confirm order, with a discount applied only during the "enter payment" step after a minimum order total is validated. If the discount-application endpoint doesn't itself re-check that total — trusting that the frontend only calls it at the right step, in the right order — a user can call that endpoint directly, skip the earlier steps, and apply the discount to an order that never actually met the minimum.
def redeem_gift_card(user, card_id, amount):
balance = db.get_card_balance(card_id)
if balance >= amount:
db.deduct(card_id, amount) # the check and the deduction are two SEPARATE operations
return successTwo concurrent requests redeeming the same gift card can both read the same balance before either one's deduction is applied — each individually sees enough balance and proceeds, and the card ends up over-redeemed beyond its actual value. This is a TOCTOU (time-of-check to time-of-use) race: the check and the action it authorizes aren't atomic, and a narrow timing window between them is exploitable by firing requests simultaneously. The fix is making the check-and-deduct a single atomic database operation (UPDATE cards SET balance = balance - %s WHERE id = %s AND balance >= %s, checking the affected-row count) instead of two separate steps with a gap between them.
A checkout request that includes "price": 0.01 for an item alongside its product ID is only a vulnerability if the server actually trusts the client-submitted price instead of looking up the real price server-side from the product ID — a surprisingly common shortcut when a frontend already "knows" the price and a backend developer reuses that same field directly. Any value with real financial or permission consequences — price, quantity limits, a user's own role — must be derived or re-validated server-side, never trusted from client input just because the client happened to submit a plausible-looking value.
Finding a SQL injection just requires recognizing "untrusted input concatenated into a query" as a pattern, independent of what the query does. Finding a business logic flaw requires understanding what the workflow is actually supposed to enforce — "a discount only applies above a minimum order," "a gift card can't be redeemed past its balance" — and then checking whether the code genuinely enforces that rule everywhere it claims to, or only in the one place a well-behaved client happens to call it from.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why are business logic vulnerabilities invisible to technical vulnerability scanners?
2. What is a TOCTOU (time-of-check to time-of-use) race condition?
3. Why is trusting a client-submitted price field a real vulnerability?
Application Security & Secure Coding