If you ask a security tool to scan your app, it'll dutifully look for SQL injection, XSS, broken authentication — the OWASP Top 10. That's a useful checklist. But the bugs that actually cause direct financial loss are usually not on it.
They're business logic flaws. And because they look like "the application working as designed," they're invisible to anything except a human reading the spec.
The pattern
Business logic flaws happen when an application enforces only the technical rules and forgets the business rules. The code does exactly what it was written to do. The problem is that what it was written to do is exploitable.
A few examples we see repeatedly:
The negative quantity
An e-commerce checkout that doesn't validate quantity can accept -5 items. The total goes negative. The user receives a refund instead of paying. The cart looked fine on the frontend; the backend trusted it.
The race for the same coupon
A coupon flagged as "single-use" is checked against the database, then marked as used in a second query. Fire 50 requests in parallel and 50 of them pass the check before the first one writes the flag. The user gets the discount 50 times.
The forgotten state transition
An invoice has states: draft → sent → paid → refunded. The "approve refund" endpoint checks that the invoice exists, but not that its current state is "paid." An attacker calls it on a "draft" invoice and money flows out for an order that was never placed.
The trust chain across endpoints
"Set delivery address" requires the user to own the order. "Mark order as delivered" doesn't, because it's only supposed to be called internally by the warehouse service. An attacker calls it directly, marks orders as delivered, and abuses the chargeback flow.
Why these bugs are different
You cannot find these by scanning for patterns. There's no signature for "this function should have checked the invoice state." They require someone to understand what the application is for and to ask, "what would happen if I did this thing the developer didn't expect?"
This is also why these bugs survive even the most mature security tooling. SAST, DAST, dependency scanning, fuzzing — none of them know your business rules.
The defenses
The good news: a few patterns kill most business logic bugs at once.
- Validate state machines explicitly. Every action should check both the actor and the target's current state.
- Authorize at the data layer, not the controller layer. If access checks live with the entity (e.g., "an invoice can only be refunded if it's in a 'paid' state by the org that paid it"), they can't be bypassed by a forgotten endpoint.
- Reject ambiguity at the boundary. Negative quantities, zero amounts, dates in the past — if they're never legitimate, refuse them at the input layer.
- Use database transactions and idempotency keys for any state change involving money or limits. This kills entire classes of race condition.
None of this is novel. It's just hard to remember on a deadline. Which is exactly why an outside set of eyes — someone whose only job is to ask "what would break here?" — pays for itself, often within a single engagement.