3 min readBUSINESS-LOGIC · METHODOLOGY · WEB

Business logic is where scanners lose

Why automated tools never find logic bugs, and the way I map a money or quota flow to attack its invariants instead of its inputs.

Generic write-up, no targets. Every example below uses example.com, an attacker account, and a victim account. The point is the class, not any one finding.

Why the scanner is blind here

A vulnerability scanner works by pattern. It sends a payload, looks for a tell (an error string, a time delay, a reflection), and matches it against a rule someone wrote in advance. That model works for injection and known CVEs because the bug is in the plumbing and the plumbing is the same everywhere.

Logic bugs are in the policy. “A coupon may be used once.” “Quantity must be a positive integer.” “You cannot withdraw more than your balance.” Those rules are specific to this app, written by this team, and there is no payload that reveals them. The scanner does not know the rule exists, so it cannot know the rule was broken. You have to read the flow like a human and ask what the developer assumed.

Map the flow, then list its invariants

Before I touch anything I draw the money or quota path end to end: which endpoint sets the price, which one applies the discount, which one charges, which one fulfils. Then for each step I write the invariant, the thing the code believes is always true. The bug is almost always a step that trusts an invariant a previous step was supposed to guarantee but never re-checked.

Attack the invariant, not the input

Here is the mental switch. A scanner attacks inputs: it mutates a value and watches for a crash. I attack invariants: I find a thing the code assumes is true and arrange for it to be false at the moment it matters.

Take a checkout. The price is computed at one endpoint and charged at another.

http
POST /api/cart/quote HTTP/2
Host: example.com

{"sku":"PRO-PLAN","qty":1}
http
{"quote_id":"q_8841","unit_price":4900,"qty":1,"total":4900}

The invariant is “the total I charge equals the quote I issued.” If the charge endpoint re-reads total or qty from the client instead of from the stored quote, I break it:

http
POST /api/cart/checkout HTTP/2
Host: example.com

{"quote_id":"q_8841","qty":1,"total":1}

If it charges 1 cent, the second step trusted a number the first step owned. Same logic for negative quantity (the total goes negative and the gateway issues a credit), for fractional quantity (the per-unit math rounds in my favour), and for applying a coupon to quote_id after the total was frozen.

How I prove it without hand-waving

A logic bug is only real if the invariant is observably broken, so I show the before and the after: the legitimate quote, the manipulated charge, and the account balance or order record that proves money or quota moved the wrong way. For state-machine skips I show the sequence of requests out of order and the fulfilment that should have been impossible. No payload, no CVE, just a clean demonstration that the application did something its own rules forbid.

The transferable habit is this. When I land on a target with payments, subscriptions, credits, or limits, I do not start fuzzing. I start by writing down what the developer believes can never happen, and then I make it happen. That is the whole game, and it is exactly the part a scanner cannot reach.