Business-logic flaws: the bugs a scanner can't see
Some of the most damaging findings we report aren't "vulnerabilities" in the classic sense at all. Nothing is injected, nothing is misconfigured. The application simply allows a sequence of legitimate actions that, together, do something it never should — buy for nothing, approve your own request, move money without a second signature. These are business-logic flaws, and they're invisible to tools because nothing is technically broken.
What they look like
- Workflow bypass. A record jumps straight to a terminal "completed" state without the prerequisite steps — and its date is set to whenever the attacker chooses.
- Missing segregation of duties. One operator can both initiate and approve a high-impact action — rerouting or halting payments — with no second actor required.
- Price and discount abuse. Stacking coupons, negative quantities, or currency edge-cases to pay far less than intended.
- Role-label mismatch. A role presented as harmless "self-service" actually grants administrative power.
Why scanners can't find them
A scanner looks for known-bad patterns: a payload that triggers an error, a header that's missing, a version that's vulnerable. A business-logic flaw has none of those tells. Finding it requires understanding what the application is for — the rules it's supposed to enforce — and then deliberately trying to break those rules while every individual request looks perfectly valid. That's a human activity. It's where manual testing stops being a more thorough scan and becomes something tools can't replicate.
How to defend
- Enforce state transitions server-side. The server, not the client, decides what order things can happen in.
- Validate client-supplied values for plausibility. Dates, quantities, prices and statuses from the client are suggestions, not facts.
- Design in segregation of duties. High-impact actions need a second actor; make it a rule in the system, not a policy in a document.
- Test the logic explicitly. Ask your testers to attack business rules, not just the OWASP Top 10.
Why they survive code review, too
Business-logic flaws slip past code review for the same reason they slip past scanners: every individual line is correct. The bug doesn’t live in a function; it lives in the interaction between steps that are each, on their own, doing exactly what they were asked. Catching it needs someone thinking adversarially about the whole workflow at once — asking “what if these happen out of order, or with values the designer never imagined?” That’s precisely what manual testing provides, and it’s why, before we test, we ask clients to tell us what the application is supposed to prevent — not just what it’s supposed to do.
Testing logic needs domain context
We’re at our most effective on business logic when the client simply tells us the rules the application is supposed to enforce — the maker/checker requirement, the order the steps must happen in, the limits that should hold. Armed with the intended logic, breaking it becomes methodical. Without it, even a skilled tester is partly guessing at what “wrong” means. The best logic testing is a collaboration: you describe the rules, we try to break them.
Race conditions, TOCTOU and replay: when timing is the vulnerability
Most of the flaws in this piece are about what a request is allowed to do. A whole separate class is about when, and how many times, it is allowed to do it. A scanner fires requests one after another and sees each response in isolation; it never asks what happens when two requests arrive in the same instant, or when the same valid request is sent a hundred times before the system catches up. That gap between a check and the action it guards — time-of-check to time-of-use — is where a surprising amount of real money leaks out.
The canonical shape: a balance, a quota, a voucher or a stock count is read, judged sufficient, and then decremented — but between the read and the decrement, a second request slips through the same check against the same unchanged value. Each request individually passes. Together they overdraw. "Apply one coupon per order" becomes ten coupons when ten requests race the single check.
T0 req A: read balance = 100, need 100 -> OK
T0 req B: read balance = 100, need 100 -> OK (A has not committed yet)
T1 req A: debit 100 -> balance 0
T1 req B: debit 100 -> balance -100 value extracted twice
Replay is the same problem without the concurrency: a request that should be good once — a payment capture, a one-time token, a referral credit — is simply sent again because nothing marks it as spent. These are not implementation bugs in a single function; they are missing guarantees about atomicity and uniqueness across a workflow.
How to harden this
- Make the check and the state change atomic: a conditional update or a row lock that decrements only if the value still holds, not a read followed by a separate write.
- Give value-bearing operations an idempotency key, so a retried or replayed request resolves to the same single outcome instead of a second effect.
- Enforce uniqueness and one-time-use at the datastore, where concurrent transactions actually serialise, rather than in application code that two workers run in parallel.
- Test deliberately with bursts of simultaneous identical requests — the single-request success case tells you nothing about the race.
Modelling the workflow as a state machine
Business logic is, underneath, a state machine: an order is cart → placed → paid → shipped, an account is invited → active → suspended. Flaws appear when the implementation permits transitions the real process forbids — paying for an order that was already refunded, shipping one that was never paid, re-opening a closed dispute to claim a credit twice. The application enforces each step in isolation but never asserts that the step is legal from the current state.
The way to find these is to draw the intended machine explicitly — states as nodes, legitimate transitions as edges — and then ask, for every state, which requests the system will actually honour there. Every edge you can traverse that is not on the diagram is a finding. This is the structured cousin of forced browsing: instead of guessing URLs, you walk the process out of order and watch for a step that should have been refused.
placed --> paid --> shipped
| |
v v
cancelled refunded illegal: refunded --> shipped
illegal: shipped --> paid (double capture)
The high-value targets are transitions that move money, grant entitlements or cross a trust boundary. Confirm that each is guarded not only by "is this input valid" but by "is this transition permitted from the state the object is in right now, for this actor".
How to harden this
- Represent workflow state explicitly and validate every transition against the current state server-side, rejecting any edge not in the model.
- Guard money- and entitlement-moving transitions with the strictest checks, and make them idempotent so a repeat cannot re-grant.
- Keep the state machine in one authoritative place rather than re-deriving "what's allowed next" at each endpoint, where the rules drift apart.
Parameter tampering against implicit trust
Logic flaws also hide in the assumption that whatever the client sent reflects what the client was offered. Prices, quantities, discounts, tier identifiers, recipient accounts and shipping totals that arrive in the request are frequently trusted because the legitimate front end would never send a bad one. The attacker is not using the legitimate front end. Negative quantities that credit rather than charge, a price field echoed back from a hidden form value, a discount percentage raised past 100, a tier upgraded by editing one parameter — each is a case of the server trusting a number it should have computed or re-validated itself.
The discipline is to treat every security- or value-relevant quantity as something the server owns. The client may say which product and how many; the server decides the price, re-checks the bounds, and recomputes the total from authoritative data. Anything the client is allowed to assert must be validated against what it was actually permitted to assert.
How to harden this
- Recompute prices, totals, discounts and entitlements server-side from trusted records; never accept them from the request as fact.
- Bound every quantity (no negatives, sane maxima) and validate client-asserted identifiers against what that actor is entitled to select.
- Do not round-trip sensitive values through hidden fields or client state where they can be edited in flight.
Testing your own logic before someone else does
Because these flaws are specific to how your process works, no off-the-shelf tool finds them for you — but your own team can, cheaply, before an assessment does. The method is a conversation, not a scanner: sit the people who understand the business rules next to the people who understand the requests, and for each rule ask "what happens if a user simply doesn't follow it?"
- Enumerate the rules as sentences. "A coupon applies once." "You can't refund more than you paid." "Only the owner cancels the booking." Each sentence is a test: try to violate it directly against the API.
- Walk the state machine out of order. Take a real object through its steps, then attempt each step from the wrong state and from a second account's session.
- Race the value-bearing actions. Fire simultaneous and repeated identical requests at anything that spends, credits, redeems or reserves, and reconcile the final state against what should have happened.
- Tamper with every number. Replay requests with edited prices, quantities, identifiers and totals, and confirm the server recomputed rather than trusted.
Write these as repeatable checks that assert the outcome — final balance, order status, entitlement granted — not merely the HTTP code, and run them in CI. A logic flaw that a regression test can catch is one an attacker never gets to report.
Where's the logic flaw in your app?
Manual penetration testing targets the business-logic and segregation-of-duties gaps that scanners can't see.
Request a proposal