Business Logic Vulnerability Examples: Five Valid Requests That Break the Rules

Business Logic Vulnerability Examples: Five Valid Requests That Break the Rules

A business logic vulnerability is a flaw in the rules an application follows rather than a flaw in how it parses input. The request is valid, the session is real, every field has the right type, and the server still ends up doing something it was never meant to do. This post collects five business logic vulnerability examples from an invented shop, shows the requests that cause them, and explains why this class of bug survives the tools most teams already run.

Five business logic vulnerability examples

The examples below all come from Acme Store, an invented ecommerce app with a cart, coupons, refunds, and a free trial. Nothing here is injected or malformed. Each request is one a normal client could send.

1. The client sends the price

The add to cart request carries the product price, and the server reads it straight from the body instead of looking it up.

POST /api/cart/items
Content-Type: application/json

{ "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 }

200 OK
{ "cart_total": "1.00" }

Nothing about this request is invalid. The price field is a number, in range, correctly typed. The app is broken because it accepted a value that only its own catalog should decide.

2. A negative quantity turns a purchase into a credit

Quantity is validated as an integer. Nobody stated that it must be above zero.

POST /api/cart/items
{ "sku": "LAPTOP-15", "quantity": 1 }
{ "sku": "MOUSE-01", "quantity": -20 }

200 OK
{ "cart_total": "-98.00" }

A negative line item subtracts from the total. Depending on how the payment step handles a negative amount, this either discounts the order or issues money. The type check passed. The rule that a basket cannot contain less than nothing was never written down anywhere the code could enforce it.

3. One coupon applied many times

A discount code is marked single use, and the check is done by reading the coupon, confirming it is unused, and then marking it used. Two requests arriving at the same moment both pass the read before either writes.

POST /api/cart/coupon   { "code": "SAVE20" }
POST /api/cart/coupon   { "code": "SAVE20" }      sent in parallel
POST /api/cart/coupon   { "code": "SAVE20" }

200 OK
{ "discounts_applied": 3, "cart_total": "12.00" }

This is the classic gap between checking a condition and acting on it. Each request individually obeys the rule. The rule only holds if the check and the update happen as one atomic step, which is a database property, not a validation property.

Input validation asks whether a value is well formed. Business logic asks whether a well formed value still makes sense. Most applications only answer the first question.

4. Skipping a step in the order flow

Checkout is meant to run in order: create the order, take payment, then confirm. The confirmation endpoint trusts that the earlier steps happened, because in the interface they always do.

POST /api/orders            { "cart_id": 55 }        creates order 9001, status pending_payment
POST /api/orders/9001/confirm

200 OK
{ "id": 9001, "status": "confirmed", "paid": false }

The payment call is simply never made. The server moved the order to confirmed because it was asked to, without checking that the state it was moving from allowed that transition. Any multi step flow with a state field is worth testing this way, including onboarding, verification, and approval workflows.

5. Resetting a free trial that was meant to be once per person

Acme Store gives one trial per email address and checks for an exact match on the stored string.

POST /api/signup   { "email": "sam@example.com" }      trial granted
POST /api/signup   { "email": "Sam@Example.com" }      trial granted again
POST /api/signup   { "email": "sam+2@example.com" }    trial granted again

The identity the business cares about is the person. The identity the code compares is a string. Whenever those two differ, a limit that reads as once per customer becomes once per spelling. The same shape appears in referral bonuses, per user rate limits, and vote counting.

Why these are hard to catch automatically

Every example above produces a clean 200 OK. There is no payload, no error, and no anomaly in the logs beyond a slightly odd number. A tool that works from a list of known bad strings has nothing to match on, because the input is data the app was built to accept.

Catching these needs knowledge that lives outside the code: a coupon applies once per order, a basket cannot hold negative items, an order is confirmed only after payment. Those are assumptions, and an assumption nobody wrote down is an assumption nobody enforced. That is also why these bugs tend to be found by people who first learned how the product is supposed to work. More on the basics behind these bugs is here.

How to find them

  • Write the rules down first. For each feature, list what must always be true: one coupon per order, quantity above zero, refund never exceeds the amount paid. You cannot test an invariant you have not stated.
  • Then try the opposite of each one. Send the coupon twice, the quantity negative, the refund larger than the charge. The test is only useful if it attacks the rule directly.
  • Replay and reorder requests. Capture a normal flow, then send its steps out of order, twice, or in parallel. Skipping a step and repeating a step are two different bugs.
  • Change values the interface never lets you change. Prices, ids, totals, roles, and status fields are the ones worth trying, because the client is not meant to control them.
  • Test the identity, not the string. Try case changes, plus addressing, trailing spaces, and unicode variants against any per person limit.

How to fix them

  • Derive money and permission on the server. Look up the price from the catalog, and never accept a total, a discount, or a role from the client.
  • Make the check and the write atomic. A conditional update or a unique constraint enforces single use, while a read followed by a write does not.
  • Enforce transitions, not just states. Confirm should refuse to run unless the order is in a state that allows it, checked in the same statement that performs the change.
  • Normalize before you compare. Decide what counts as the same person, then apply that rule at every place the limit is enforced.
  • Turn each confirmed bug into a standing test. These regress quietly during refactors, because nothing about them looks like security code.

Business logic flaws are the bugs that require understanding the application rather than recognizing a pattern, which is why they are underrepresented in scanner reports and overrepresented in real incidents. Testing them means forming an idea about what an app assumes and then designing a request that breaks that assumption, which is exactly what an autonomous researcher built around application logic is meant to do. Read more about how UnboundCompute works.

Frequently asked questions

What is an example of a business logic vulnerability?

A common one is a price the client is allowed to set. If the add to cart request contains { "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 } and the server reads that price instead of looking it up in its own catalog, the buyer decides what things cost. Every field is the right type and the request is completely legal, which is what separates this class from injection bugs.

How is a business logic bug different from a technical vulnerability?

A technical vulnerability such as SQL injection or cross site scripting comes from input the application failed to handle safely, so there is a bad string to look for. A business logic bug comes from valid input used in a way the designers did not consider, so there is nothing wrong with the request itself. The first is a parsing problem and the second is an assumption problem, which is why they are found by different methods.

Why do automated scanners miss business logic flaws?

Scanners compare traffic against a list of known bad patterns, and these requests contain none. Sending a coupon three times, ordering a negative quantity, or confirming an order before paying all produce a clean 200 OK. To call any of those a bug you need to know the rule that was broken, such as one coupon per order, and that rule usually exists only in someone’s head or in a product document rather than in the code.

How do I test for business logic vulnerabilities?

Start by writing down what must always be true for each feature, then design a request that attacks each statement directly. Send the single use coupon in parallel with itself, set a quantity below zero, confirm an order without paying, and sign up again with a different spelling of the same email. Replaying, reordering, and skipping steps in a captured flow finds most of them, because these bugs live in sequence and state rather than in any single request.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.