What is a business logic vulnerability?
A business logic vulnerability is a flaw in the rules an application is supposed to enforce, not in its code syntax. Every request is well-formed and every input is a value the field accepts. The attack is in the combination: a sequence the designer did not expect, a value outside the range they assumed, or a step reached without the ones that should precede it. Because nothing is malformed, there is no payload to detect. The application does exactly what it was told, which is the problem.
These flaws are specific to what the application does. A checkout, an approval workflow, a coupon system, and a funds transfer each have their own rules, so each has its own ways to break them.
What do they look like in practice?
Workflow skipping
A multi-step flow assumes the steps happen in order and each one is authorized. If the final step can be requested directly, the guards on the earlier steps never run.
POST /checkout/complete HTTP/1.1
Host: acme.example
Content-Type: application/json
Cookie: session=user_4821
{"cart_id": "cart_90b1", "order_id": "ord_5567"}If this succeeds without a prior POST /checkout/payment for the same order, the application shipped goods that were never paid for. The payment step existed; nothing forced it to run.
Negative and boundary quantities
Code that trusts a quantity or amount to be positive can be handed a negative one. A refund path, a cart, or a transfer that multiplies price by quantity may produce a credit:
POST /cart/items HTTP/1.1
Host: acme.example
Content-Type: application/json
Cookie: session=user_4821
{"sku": "SKU-114", "quantity": -3}If the total is computed as price times quantity with no lower bound, a negative quantity subtracts from the total, and a large negative one can drive it below zero into a refund. The same pattern appears in loyalty points, gift-card balances, and account transfers.
Coupon and discount stacking
A single discount is intended, but the code checks each coupon's validity but never enforces one per order. Applying SAVE20 three times, or combining a percentage coupon with a fixed-amount coupon the rules meant to be mutually exclusive, can drive the price to zero or below. Reapplying the same code after removing and re-adding an item is a common variant.
State machine abuse
An object has states the application moves it through: an order is pending, then paid, then shipped, then delivered. If a transition can be requested out of order, an attacker cancels an order after it ships for a refund on kept goods, or reopens a closed dispute, or moves a subscription to active without the paid transition. This overlaps with a race condition when two conflicting transitions are fired at once.
Why do scanners miss them?
Automated scanners look for patterns: a payload that produces an error, a response that reflects input, a signature of a known bug. A business logic flaw has none of those. The negative-quantity request returns 200 OK with a valid order. The skipped-payment request looks like a normal completion. To flag it, a tool would need to know that this application charges before shipping and that a negative quantity should be impossible here, which is domain knowledge the tool does not have. This is why logic flaws rarely appear on a checklist and why they survive DAST and SAST runs that pass clean.
How do testers find them?
The method is to understand the intended flow, then deliberately violate its assumptions.
- Map the workflow. Walk each multi-step process and note every state, every transition, and every assumption: what must be true before this step, what range each value is supposed to occupy, which actions are meant to be mutually exclusive.
- Attack the sequence. Request the final step first. Repeat a step that should run once. Run steps out of order. Replay a request from a completed flow against a new object.
- Attack the values. Send negative numbers, zero, very large numbers, and fractional amounts where integers are assumed. Send a currency or SKU the caller should not be able to select. Change an identifier to another user's object, which crosses into broken object-level authorization.
- Attack the constraints. Apply a coupon twice, stack exclusive discounts, and exceed a per-account or per-order limit. Test whether a limit is enforced or merely displayed.
- Attack concurrency. Fire two conflicting requests at once to see whether a check-then-act step can be raced, covered in the race condition entry.
The tester's advantage is a model of what the feature is for, which is exactly what a signature-based tool lacks.
How do you fix them?
There is no single patch, because the flaw is per-feature. The principles that hold:
- Enforce rules on the server, in one place. Never trust the client to have run the prior step or to send a sane value. Re-check the current state and the invariants at the point of action.
- Validate ranges and types explicitly. Reject negative and zero quantities, cap maximums, and constrain currencies and SKUs to allowed sets.
A Django view that guards a state transition and a quantity bound server-side:
from django.db import transaction
from rest_framework.exceptions import ValidationError
@transaction.atomic
def complete_checkout(request, order_id):
order = Order.objects.select_for_update().get(
id=order_id, user=request.user
)
if order.status != Order.PAID:
raise ValidationError("order is not paid") # no skipping the payment step
for item in order.items.all():
if item.quantity < 1:
raise ValidationError("invalid quantity") # no negative or zero
order.status = Order.FULFILLED
order.save()
return order- Model transitions as a state machine and reject any transition that is not legal from the current state; do not scatter ad hoc state checks through the code.
- Make constraints atomic. Enforce "one coupon per order" and per-account limits inside a transaction with a row lock or a unique constraint, so two concurrent requests cannot both pass the check.
A WAF cannot fix this. Every request is legitimate traffic, so there is no rule to write. The fix lives in the application's own enforcement of its rules.
[ Sources ]
Written by Parameter · Last reviewed

