What Business Logic Vulnerabilities Are (and Why They're Different)
Business logic vulnerabilities are flaws in the design and implementation of application workflows β the rules that govern what users can do, in what order, with what inputs. Unlike SQL injection or XSS, there's no "bad function call" to grep for. The code often works exactly as written; the problem is that the specification was wrong, incomplete, or didn't anticipate adversarial input.
Examples: an e-commerce site that allows negative quantities in a cart, a loyalty system that allows earning points on refunded purchases, a multi-step signup that doesn't enforce step order, a discount system that allows stacking unlimited coupons. Each of these is technically correct code implementing an incorrect rule.
High financial impact: Business logic flaws in financial and e-commerce applications directly translate to monetary loss. Unlike XSS (which requires user interaction) or SQLi (which requires database access), price manipulation can be exploited by any authenticated user at scale.
Price and Quantity Manipulation
Any application that trusts client-supplied prices, quantities, or totals is vulnerable. The price of a product should always be read from the server's database at checkout time β not from the request body.
# Vulnerable β price comes from client request @app.post("/api/checkout") def checkout(body: dict): items = body["items"] # [{product_id: 1, quantity: 2, price: 0.01}] total = sum(item["price"] * item["quantity"] for item in items) charge_card(total) # Vulnerable β negative quantity causes negative total (credit card refund) # [{product_id: 1, quantity: -5, price: 99.99}] β total = -$499.95 # Fixed β always look up price from DB; validate quantity range @app.post("/api/checkout") def checkout(body: dict, current_user = Depends(get_current_user)): total = 0 for item in body["items"]: product = db.get_product(item["product_id"]) # price from DB quantity = item["quantity"] if quantity < 1 or quantity > 100: # validated range raise HTTPException(status_code=400, detail="Invalid quantity") total += product.price * quantity charge_card(total)
We've seen negative quantities cause refunds: An e-commerce platform processed a negative quantity cart item and issued a credit to the attacker's payment method. The bug had been in production for two years before a security audit found it β automated scanners had never flagged it because the code was syntactically correct.
Privilege Escalation Through Workflow Bypass
Multi-step processes often have security checks at certain steps but assume previous steps completed correctly. If an attacker can skip directly to a later step, those earlier checks are bypassed.
Classic example: a user self-upgrade flow that requires email verification, then payment, then role assignment. If the role assignment endpoint only checks that the user is logged in (not that they completed payment), an attacker can skip payment and go straight to the role assignment call.
# Step 3: Assign premium role β vulnerable if payment step can be skipped @app.post("/api/upgrade/confirm") def confirm_upgrade(current_user = Depends(get_current_user)): # Only checks login β not whether payment was completed current_user.role = "premium" db.commit() # Fixed β check that payment was actually processed @app.post("/api/upgrade/confirm") def confirm_upgrade(current_user = Depends(get_current_user)): pending = db.get_pending_upgrade(current_user.id) if not pending or not pending.payment_confirmed: raise HTTPException(status_code=403, detail="Payment not verified") current_user.role = "premium" db.commit()
Race Conditions in Business Logic
Race conditions occur when two concurrent requests interact with shared state before either has completed β leading to inconsistent results. In business logic, this often means double-spending, double-redemption, or limit bypass.
# Vulnerable to race condition β two concurrent requests can both pass the check def apply_coupon(user_id: int, coupon_code: str): coupon = db.get_coupon(coupon_code) if coupon.used_by is not None: # check raise Exception("Already used") # Both threads pass the check before either marks it used time.sleep(0.01) # simulate DB write latency coupon.used_by = user_id # act β both threads write, coupon used twice db.commit() # Fixed β atomic update with conditional write def apply_coupon(user_id: int, coupon_code: str): updated = db.query(Coupon).filter( Coupon.code == coupon_code, Coupon.used_by == None # condition in the UPDATE itself ).update({"used_by": user_id}) db.commit() if updated == 0: raise Exception("Coupon already used or invalid")
The fixed version uses a single atomic UPDATE with the condition embedded β the database's row-level locking ensures only one update succeeds. The check-then-act pattern (read, then write based on read) is the root cause; atomic conditional writes eliminate it.
Coupon and Discount Abuse
Beyond race conditions, coupon systems need to think about: Can the same code be applied to the same order multiple times? Can codes be applied after an order is already paid? Can a refund re-enable a used code? Can codes be guessed or enumerated? Each of these is a business logic question that automated tools won't answer for you.
Skipping Required Steps in Multi-Step Flows
Any multi-step process β registration, checkout, KYC, account verification β should track state server-side. Never rely on client-side state or parameter-based step indicators to know where the user is in a flow. An attacker can send any request to any endpoint regardless of what the frontend shows.
- Store flow state in the session or database, keyed to the user
- Each step should verify that previous required steps completed successfully
- Expiry matters β an incomplete flow from 24 hours ago shouldn't be completable today
Why Automated Scanners Miss These
SAST tools analyse code patterns and data flow. Business logic flaws don't have a "pattern" β they're semantic errors in how the application models the real world. The code to apply a coupon may be syntactically and structurally perfect; the flaw is that it doesn't handle concurrent requests atomically.
DAST tools send requests and look for unexpected responses β but they don't understand your specific business rules. A scanner doesn't know that a negative quantity should be rejected; it only knows HTTP response codes.
This doesn't mean scanners are useless here β they provide a baseline that frees security engineers to focus on these harder manual tests. But the business logic layer requires human understanding of the application.
How to Find Business Logic Flaws
- Threat model your flows β for every business process, ask "what if someone does this out of order?" and "what happens with boundary values?"
- Involve developers in security review β they know the intended flow best and can spot where implementation diverges from spec
- Test negative quantities, zero values, and maximum integers β in any numeric input
- Test concurrent requests β send the same request twice simultaneously for any one-time operations
- Test step skipping β try accessing step N without completing step N-1
- Replay old tokens and codes β verify one-time-use enforcement
Testing Checklist
- Validate all numeric inputs server-side β range checks, sign checks, maximum values
- Never trust client-supplied prices, quantities, or totals β always look up from server-side data
- Use atomic DB operations for one-time actions β conditional UPDATE, SELECT FOR UPDATE, or distributed locks
- Track multi-step flow state server-side β never in URL parameters or client state
- Enforce step ordering explicitly β each step checks that prerequisites completed
- Test concurrent requests for all critical operations β coupon redemption, account upgrades, limited inventory purchases
- Conduct code walkthroughs with business context β pair dev with security engineer to walk critical flows
- Bug bounty for logic flaws β external researchers with adversarial mindset often find what internal teams miss
Catch What Scanners Miss
AquilaX combines SAST, DAST, and API security testing to give you the broadest automated coverage β freeing your security team to focus on the logic flaws that need human review.
Start Free Scan