Get a quote

Field guide · Application security

Application security

When the code works exactly as written.

By Abhimanyu Gupta, Founder & Principal Operator

The most expensive flaws we find are not broken code. They are valid code doing precisely what it was told, and something the business never intended. There is no crash, no error, no malformed input. Every line runs as designed. The design simply did not anticipate an adversary who reads the workflow as a set of moves rather than a set of instructions. This is a field guide to business-logic flaws: what they are, why no scanner will ever find them, the shapes they take, and how to close them.

At a glance

Attack class
Business logic abuse (valid code, unintended outcome)
Typical impact
HIGH  Direct financial loss, fraud, integrity failure, abuse of limits
Mapping
CWE-840 Business Logic Errors. OWASP WSTG, Business Logic Testing
Access required
Usually one ordinary user account and an understanding of the domain
Prevalence
Common and high-impact, and almost entirely invisible to automated tools
Primary tooling
Manual testing, Burp Repeater, Turbo Intruder for races, a clear head
Reference
OWASP Testing for Business Logic; PortSwigger Web Security Academy

A bug in the requirements, not the code

A negative quantity that credits an account. A coupon that stacks forty times. A checkout step you can skip. A refund you can approve for yourself. None of these is a coding error in the usual sense. The function receives input it accepts, runs the logic it was given, and returns a result it considers correct. The flaw is that the logic encodes an assumption the developer held but never enforced: that quantities are positive, that steps happen in order, that a coupon is used once, that the person approving a refund is not the person receiving it.

Business-logic flaws live in that gap between what the code checks and what the business actually requires. They are, quite literally, the code working exactly as written, in a way nobody wrote down was wrong.

The distinction that matters. A SQL injection is the code doing something it was never meant to do. A business-logic flaw is the code doing something it was meant to do, for someone it was not meant to do it for, or in an order it was not meant to allow. The first has a signature. The second has only intent, and intent is not in the source.

Why no scanner finds them

Static analysis reasons about code: data flow, tainted inputs, dangerous sinks. Dynamic scanners reason about responses: error strings, reflected payloads, status codes. Neither reasons about your business. A tool cannot know that an order total should never be negative, that a loyalty point should not be spent twice, or that a KYC step is mandatory before a withdrawal, because those are facts about your domain, not your syntax.

This is why a clean automated scan says almost nothing about logic. The scanner is not failing; it is being asked a question it has no way to answer. Finding these flaws requires a human who first learns what the application is for, then asks what a motivated user could bend it into. That is judgment, and judgment is the one thing you cannot buy in a license.

The shapes logic flaws take

Logic flaws feel infinite because every application has different rules, but they cluster into recognizable patterns. Knowing the patterns is how you test an app you have never seen before.

PatternThe unenforced assumptionExample abuse
Input boundsValues fall in a sane rangeNegative quantity credits the balance; a huge value overflows a limit
Workflow orderSteps happen in sequenceSkip payment and land on the confirmation endpoint directly
Single useA token or action is used onceReplay a one-time voucher, referral, or refund request
Client trustThe client will not tamperPrice, discount, or role sent from the browser and believed
AggregationDiscounts and limits do not composeStack coupons until the cart total goes below zero
TimingChecks and actions are atomicRace two requests so both pass the same balance check

Race conditions: the timing flaw

The timing pattern deserves its own section because it is the most missed and often the most damaging. A race condition, in this context a time-of-check to time-of-use flaw, happens when an application checks a condition and then acts on it as two separate steps. If an attacker fires many requests in the same instant, several can pass the check before any of them commits the action, and the limit the check was protecting is overrun.

The classic case is a balance of 100 and two simultaneous withdrawals of 100. Each reads the balance, sees 100, and approves. The account ends at minus 100. The same primitive drains gift-card balances, redeems a one-per-customer offer many times, or overruns any "you may do this N times" limit. We test it by sending a burst of identical requests as close to simultaneously as the transport allows and watching whether the invariant holds. When it does not, the fix is atomicity: a database lock, a conditional update, or an idempotency key, so the check and the action cannot be split.

Concurrency is an input. Most testing sends one request and reads one response. Race conditions only appear under parallel load, which is exactly why functional tests and scanners miss them. If a limit matters, it must be tested with many requests at once, not one at a time.

A worked example

Here is an input-bounds flaw as it appears in a report. The application validates that a quantity is a number, but not that it is positive, and computes the line total by multiplying quantity by price. A negative quantity turns a purchase into a credit.

business logic, negative quantityhttp
# The client normally sends quantity 1. We send -5.
POST /api/cart/items HTTP/2
Host: shop.example.com
Content-Type: application/json

{"sku":"WIDGET-01","price":"40.00","quantity":-5}

HTTP/2 200 OK
{"line_total":"-200.00","cart_total":"-160.00"}
# The server multiplied -5 by 40 and reduced the total. Check out and the
# negative line becomes store credit or a refund to the card. Valid code,
# invalid outcome. No scanner flags a 200 OK with a well-formed body.

Notice what is not here: no error, no injection, no broken parser. The request is well formed and the response is a success. The only thing wrong is that the outcome violates a rule the code never checked.

How we find them

The method is domain-first. We learn what the application is for, then we write down the invariants it must never break: totals are non-negative, a voucher is spent once, a withdrawal cannot exceed a balance, a step cannot be reached without the one before it. Then we attack each invariant directly.

  • Read the workflow as a state machine. Map every step, then try to reach later states without the earlier ones, replay steps, and run them out of order.
  • Push every value past its intended range. Negatives, zero, enormous numbers, fractional where integers are assumed, and the boundaries in between.
  • Assume the client lies. Anything the browser sends, price, discount, role, user id, we change and resend, because if the server trusts it, that is the bug.
  • Test under concurrency. Any limit, quota, or one-time action gets a burst of parallel requests to check for the timing flaw.

Why they are so expensive

Logic flaws map directly onto money and trust. They are not "an attacker might eventually pivot to impact." The impact is the flaw: free goods, doubled payouts, drained balances, bypassed limits, fraud at scale. They are also hard to spot in production, because every request looks legitimate in the logs. There is no exception to alert on, no signature for a WAF to match. The first sign is often the reconciliation that does not balance, weeks later. That combination, high direct impact and low natural visibility, is why we treat business logic as a first-class testing objective rather than an afterthought.

How to close them

There is no single control, because there is no single flaw. The discipline is to make the code enforce the rules the business assumed.

  • Enforce invariants on the server. Validate ranges, ownership, and state transitions where the decision is authoritative, never in the client.
  • Make sensitive operations atomic. Use database transactions, row locks, conditional updates, or idempotency keys so a check and its action cannot be split by a race.
  • Model the workflow explicitly. A real state machine that rejects out-of-order and skipped steps beats implicit ordering that assumes the happy path.
  • Recompute, never trust. Prices, discounts, totals, and entitlements are derived server-side from authoritative data, not accepted from the request.
  • Add invariant monitoring. Alert when a total goes negative, a one-time token is seen twice, or a limit is exceeded, so abuse surfaces in hours, not at quarter close.

What a logic flaw is, and is not

  • It is not a coding defect in the traditional sense. The code is correct against its written spec. The spec was incomplete.
  • It is not findable by tools alone. A clean scanner report is silence on logic, not a clearance.
  • It is not low impact. These are among the costliest bugs we find, because they convert directly into fraud and financial loss.
  • It is not exotic. Most logic flaws are simple once seen. The difficulty is seeing them, which takes understanding the business, not just the bytes.

Key takeaway

A business-logic flaw is the code doing exactly what it was written to do, in a way the business never meant to allow. The vulnerability is an unenforced assumption, and assumptions are not in the source for a scanner to find.

Test them by learning the domain, writing down the invariants, and attacking each one directly, including under concurrency. Close them by enforcing those invariants on the server, atomically, and never trusting the client.

References & further reading

  1. OWASP, Web Security Testing Guide, Testing for Business Logic. The methodology behind this guide.
  2. PortSwigger, "Business logic vulnerabilities", Web Security Academy. Worked examples and free labs.
  3. PortSwigger, "Race conditions", Web Security Academy. The timing flaw in depth, with labs.
  4. MITRE, CWE-840: Business Logic Errors. The formal weakness class.
  5. MITRE, CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition. The concurrency pattern.
All posts Application security

Beyond the blog

Want this tested on you?

Reading about it is one thing. Seeing it proven on your own systems is another.