Web Vulnerabilities 101 (Part 3): Insecure Design, API Flaws and Business Logic — Where Scanners Give Up
The first two parts dealt with mistakes you can point at in a single line of code: a query built by string concatenation, an endpoint that forgot its authorization check, a default credential left in place. This part is harder. Here the code runs exactly as written, the configuration is clean, the scanner reports nothing green to red — and yet the idea itself was wrong from the start. No tool will tell you that letting a user change the billing email without re-authentication is a bad design.
OWASP calls this family Insecure Design, and it makes one point worth repeating: a design flaw cannot be patched by writing tidier code. Since most products today are really a collection of APIs serving web, mobile and partners at once, much of the risk has moved out of the browser and into the API layer and the business flows behind it.
This article is written for developers past the syntax stage, and for QA and PMs — who in practice spot logic flaws far more often than the security team does.
Mapping between OWASP Top 10:2021 and OWASP Top 10:2025. Source: OWASP Foundation, CC BY-SA 4.0.1. Insecure Design (A06) — the flaw that was born on the whiteboard
The distinction matters. An implementation bug means "we meant to do the right thing and got it wrong". A design flaw means "that situation never crossed our minds". Familiar examples: a password reset flow built on facts anyone can look up; a booking feature with no cap on held seats, so a bot drains the inventory; a refund path where the seller approves their own transaction; or a discount coupon shipped without anyone asking what happens if one person redeems it a thousand times.
How to prevent it
Do threat modelling while you are still drawing the flow, not the week before go-live. One hour with three plain questions — who would want to abuse this, what do they gain, which path would they try — usually beats weeks of code review. Next to every user story, write an abuse story: "an attacker tries to X in order to Y". Put security requirements directly into acceptance criteria so they get tested like any other requirement. For flows that touch money, personal data or admin rights, standardise a reusable design pattern instead of letting every team invent one. And give every resource a limit: a cap on attempts, on quantity and on validity.
2. BOLA — the same IDOR, at API scale
In the OWASP API Security Top 10, Broken Object Level Authorization sits at number one, and it is simply the IDOR from part two wearing new clothes. What changes is the blast radius: one internal API often serves the website, the mobile app, a kiosk and a partner integration, so a single endpoint that forgets to check ownership opens the data to all of them. GraphQL makes it worse, because one query can traverse several layers of relations while the permission check only happens at the outermost layer.
Prevention: put the authorization check as close to the data as possible — in the repository or service layer, not in the controller — and reuse the same check across every channel; in GraphQL, check inside each resolver. Unguessable identifiers such as UUIDs are useful, but they are obfuscation, not access control, and must never replace the ownership check.
3. Taking too much and returning too much
Two sides of the same lazy habit. Taking too much is mass assignment: you bind the whole request body straight onto a model, the user adds a role or a paid flag, and the system writes it to the database without blinking. Returning too much is the mirror image: the API returns the full record and the interface only renders three fields, while the phone number, the address and an internal note sit untouched in the response for anyone who opens the network tab.
Prevention: declare an explicit allowlist of accepted fields per endpoint instead of binding everything; use a dedicated input object and a dedicated output serializer rather than exposing the database model; treat the OpenAPI document as a contract and add automated tests asserting that responses carry nothing beyond it; and never rely on the frontend to hide data.
4. Unrestricted resource consumption — the vulnerability measured in money
An endpoint that sends an SMS one-time code with no rate limit is a tap of cash left running for an attacker. The same applies to a search API that will happily return a hundred thousand rows, a deeply nested GraphQL query, a synchronous report export, or an image feature that calls a paid AI model per request.
Prevention: apply rate limits per user, per IP and per endpoint, with tighter thresholds on expensive operations; enforce pagination limits server-side; cap GraphQL query depth and complexity; set timeouts and circuit breakers on every outbound call; move heavy work to a queue with a quota; and watch spend as a security metric, because a sudden bill spike is usually the first sign of abuse.
5. Business logic flaws and the race that lasts milliseconds
This is where good pentesters earn their fee and automated tools are useless, because no scanner knows your business rules. The classics: a price sent up from the client and trusted by the server; skipping steps in a multi-stage flow to land on the confirmation step without paying; refunding the same order twice; and race conditions — firing twenty simultaneous redemption requests while the system checks the balance once and deducts afterwards.
Prevention: recompute every monetary value on the server; store and verify workflow state server-side after each step; use database-level locking or a suitable transaction isolation level for balance deductions, with a unique constraint as the final backstop; and support idempotency keys on transaction-creating APIs so a retry never produces a duplicate. On the testing side, deliberately fire many parallel requests at the same resource — very few teams run that test, and it almost always finds something interesting.
6. When your page runs someone else's code
A typical checkout page also loads advertising, analytics, a support chat widget and a library from a public CDN. Every one of those scripts runs with exactly the same privileges as your own code: it can read the form and read the DOM. This is the route behind card-skimming incidents — nobody breaks into the victim's server, they simply modify a third-party file that everyone loads.
Prevention: minimise the number of scripts on sensitive pages; self-host critical libraries instead of pointing at a public CDN; use Subresource Integrity for external scripts; declare a Content-Security-Policy that limits allowed script sources; sandbox third-party widgets inside an iframe; and always validate the origin when receiving data through postMessage.
7. Many customers on one system
For a SaaS product the most expensive question is whether customer A's data can leak into customer B's screen. The risk rarely comes from a determined attacker; it comes from a reporting query written in a hurry without the tenant filter, or a background job running with system privileges.
Prevention: enforce tenant separation at the lowest possible layer — a default ORM scope or database row-level security — instead of trusting every developer to remember it; take the tenant identifier from the session context, never from a request parameter; write automated tests asserting that a user of one tenant cannot read another tenant's data; harden the internal admin panel, because it concentrates the most privilege and is usually the least protected surface; and mask personal data in logs and analytics pipelines.
A checklist for sign-off on a feature design
- Someone has asked out loud who would abuse this feature and how.
- Every read and write on an object verifies ownership server-side.
- Accepted input fields and returned output fields are both declared explicitly.
- Every expensive endpoint has a rate limit, a quota and a timeout.
- Monetary values and workflow state are computed and validated on the server.
- Balance-changing operations are protected against parallel execution.
- Third-party scripts on sensitive pages are reviewed and covered by SRI and CSP.
- Automated tests prove that data does not cross tenant boundaries.
Closing
The flaws in this part never show up in a scanner report. They show up in the minutes of a design meeting — or in the absence of that meeting. The good news is that they are also the cheapest class of bug to fix, provided you fix them while they are still on paper. Part four, the final instalment, turns all of these principles into operating habits: security testing inside CI, vulnerability management with real deadlines, and what to do on the day an incident actually happens.
References
- OWASP Top 10:2025 — owasp.org/Top10/2025
- OWASP API Security Top 10:2023 — owasp.org/API-Security
- OWASP Cheat Sheet Series — cheatsheetseries.owasp.org
- PortSwigger Web Security Academy — portswigger.net/web-security