OWASP Cheat Sheet Series: Business Logic Security Cheat Sheet

# Business Logic Security Cheat Sheet

## Introduction

Business logic vulnerabilities are flaws in the way an application implements its intended workflow. They aren't missing input sanitization or unescaped output. The code does what the developer told it to do, but what the developer told it to do doesn't match what the business actually needs. A user skips a required step, submits a request out of order, pays a negative price, stacks coupons in a way nobody planned for, or wins a race against the server's own bookkeeping.

No scanner will find these bugs for you. They don't have a signature to match on. They show up in code that looks perfectly fine in isolation because the bug isn't in any single function. It's in the gap between what the developer assumed and what a user can actually do.

This cheat sheet covers practical patterns for preventing business logic abuse. It's aimed at developers building features, not at penetration testers looking for them. For testing guidance, see the [OWASP Web Security Testing Guide, Business Logic Testing section](https://owasp.org/www-project-web-security-testing-guide/stable/4-Web_Application_Security_Testing/10-Business_Logic_Testing/).

Key takeaways:

- Always re-derive security-relevant values (prices, permissions, ownership) on the server. Client state is input, not truth.
- Enforce workflows as explicit state machines. Don't rely on the UI to gate the order of steps.
- Treat concurrency as a real threat. If two requests can race, assume they will.
- Rate-limit and monitor at the feature level, not just at authentication. Abuse-friendly features (referrals, coupons, password reset) need their own controls.
- Threat model from the business process, not just the technical architecture. Ask what happens if a user acts dishonestly, not just what happens if an attacker sends a malicious payload.

## Why Business Logic Flaws Are Different

Most well-known web vulnerabilities (SQL injection, XSS, CSRF, path traversal) have a clear technical signature. A security scanner can fuzz parameters, look for reflected payloads, and produce a reasonable report. Business logic bugs don't work that way.

Consider an e-commerce checkout. The server accepts a request containing a product ID, quantity, and coupon code. Every input is validated: the product ID exists, the quantity is a positive integer, the coupon code matches a known pattern. No technical rule is broken. But the application recalculates the total from the client-submitted price instead of looking it up server-side, so a user can pay one cent for a television. That's a business logic bug, and no amount of input validation helps, because the input is syntactically perfect.

The patterns repeat across industries:

- A user-facing application assumes the client will respect the order of steps in a multi-step workflow, and exposes the endpoints for each step without checking whether the previous steps were completed.
- A system takes user-controlled data (price, account balance, user role) from the request body and trusts it, because the UI never shows the user how to change it.
- An endpoint performs two operations (check balance, then debit) without a lock, so two concurrent requests both pass the balance check and both debit.
- A feature intended to reward legitimate use (referrals, points, promo credits) has no controls against one user creating many accounts.

None of these are subtle once you see them. They're just invisible to tools that don't understand the business process.

## Always Re-derive Security-relevant Values Server-side

The single most effective defensive habit is this: if a value in the request influences price, access, ownership, or state, assume the client made it up and recompute it from trusted data.

### Prices and Totals

Never accept a price, subtotal, tax, or total from the client. Accept product identifiers and quantities only, and compute the rest on the server from your own database. The same applies to discounts: accept a coupon code, validate the code server-side, and apply the discount server-side. Don't accept a "discount amount" field from the request.

| Client sends | Server should | Server should not |
|---|---|---|
| Product ID and quantity | Look up the price, compute line total | Accept a price field and trust it |
| Coupon code | Validate and apply the discount itself | Accept a "discounted total" field |
| Shipping selection | Compute cost from its own rate table | Accept a shipping cost field |
| Tax-exempt flag | Determine from account state | Accept a client-supplied exemption |

This pattern extends beyond commerce. A social app shouldn't accept a "post visibility" value from the client if the value is supposed to be derived from the user's privacy settings. A banking app shouldn't accept a "source account balance" that the client calculated.

### Permissions and Ownership

Re-check ownership on every request that acts on a resource. Don't rely on the URL, the referrer, or a flag in the request body. The backend should ask: "Is the authenticated user actually allowed to perform this action on this object, right now?"

A common mistake is checking ownership once when a resource is loaded and then trusting subsequent requests that reference it. Each request is independent. Each request must be authorized on its own.

### Identity and Role

Never accept a user ID, tenant ID, or role from the request body unless the request is explicitly an administrative action by a privileged caller, and even then the value has to be validated against what the caller is allowed to manage. The identity of the acting user always comes from the server-side session or token, never from a field the user can edit.

## Enforce Workflows as Explicit State Machines

Multi-step processes (signup flows, checkouts, approvals, KYC verification, password reset) are prime targets because developers often implement them as a sequence of pages that the UI walks the user through. If the UI is the only thing enforcing the sequence, an attacker who sends requests directly to each endpoint can skip steps, repeat steps, or reach terminal steps without the prerequisites.

### Model State on the Server

Every multi-step workflow should have an explicit state representation stored on the server, keyed to the user or session. Each transition should be validated against the current state.

A minimal pattern:

1. When the workflow starts, create a record with the initial state (e.g., `awaiting_email_verification`).
2. Each step endpoint checks the current state, performs the action, and updates the state to the next valid value.
3. If a request arrives for a step that doesn't match the current state, reject it.
4. Terminal actions (submit order, approve, transfer funds) only run if the state is exactly what's required for that action.

### Don't Use Hidden Form Fields as State

A pattern that shows up repeatedly in real applications is storing the "current step" as a hidden fi