Top 50 business logic vulnerabilities: a practitioner's reference for application security testing
Application security practitioner reference · 2026-05-24 · Written from production incident retrospectives and pentest engagement data
Why this list exists
If you're an application security engineer, an offensive security consultant, or an infosec lead trying to brief your dev team on what to actually test for, you already know the problem. The OWASP Top 10 and the CWE list are excellent at the bug-class level — injection, broken authentication, deserialisation. They are not particularly helpful at the level where most real-world breaches actually happen, which is the level of business logic flaws.
A business logic vulnerability is one that an automated scanner cannot find because the bug doesn't violate any rule in the code. It violates a rule in the business. The HTTP request is well-formed. The database query is parameterised. The authorisation check passed. And yet a user just transferred ₹1 to their own account fifteen times in three seconds and got a refund every time, netting ₹15. The code worked exactly as written. The code was wrong.
This is a reference of the fifty patterns I have personally seen exploited in production environments — fintechs, e-commerce platforms, SaaS B2B systems, payment switches, content platforms, and lending workflows. The intent is to give you a checklist that your scanner won't, so you know what to manually test for during pentests, code review, and pre-launch hardening.
The list is opinionated. It is grouped by where the bug typically lives in the application. For each item: what it is, how it gets exploited, and the test pattern that surfaces it. Where I have a strong opinion about a mitigation, I say so.
If you take only one thing from this reference: business logic is tested by trying things that should not work, then looking carefully at whether the system stopped you or just looked like it did.
Section A — Authentication and account lifecycle (1–10)
1. Account enumeration via differential responses
Login endpoint returns "Invalid password" for valid usernames and "Invalid username" for invalid ones. Or, the response timing differs by 200ms. Or, the password reset endpoint says "We've sent an email" for real users and "User not found" for nonexistent ones.
Test pattern: send identical requests with one known-valid username and one known-invalid one. Compare response bodies, status codes, headers, and timing. Any difference is a leak. Even the absence of one cookie can be the differentiator.
Mitigation opinion: same response, same timing, same headers. Period. Do not try to be clever about it.
2. Password reset that doesn't invalidate the old session
User claims their account was compromised. They reset their password. The attacker's existing session keeps working because session invalidation on password change was never implemented.
Test pattern: log in as user A in Browser 1. Reset user A's password via Browser 2. Continue using Browser 1 — if the session is still valid, you have the bug.
3. Password reset token with no expiry or no single-use enforcement
Reset email link works forever. Or it works for multiple resets. Or you can intercept the token from the URL and use it from a different device after the user has finished.
Test pattern: capture a reset token. Wait 7 days. Try it. If it works, that's the first bug. Use it twice in succession — if both work, that's the second.
4. Account recovery via knowledge-based questions
"What is your mother's maiden name?" is publicly available on social media for most people. "What city were you born in?" is on LinkedIn. KBA was security theatre fifteen years ago and is still security theatre.
Test pattern: pick a target. Spend twenty minutes on their LinkedIn, Facebook, Instagram. Attempt account recovery with what you learn. Either it works or you have your finding.
Mitigation opinion: if you must offer self-service recovery, use an out-of-band channel (verified phone, hardware token, identity-verified document upload). Eliminate KBA entirely. Hold the line against product managers who want to add it back.
5. Multi-factor enrolment that doesn't require existing authentication factor
User logs in with password. Navigates to "enable MFA." System lets them register a new TOTP without re-verifying current password or current MFA. This means anyone with a stolen session cookie can permanently lock the legitimate user out by enrolling their own authenticator.
Test pattern: log in. Attempt to add MFA. If no re-authentication is required, that's the finding. Equivalent for changing MFA, removing MFA, changing email, changing phone.
6. MFA bypass via "remember this device"
User enables MFA. Logs in on Chrome with "remember this device for 30 days." Attacker steals the persistent cookie. Now the attacker is past MFA for 30 days.
Test pattern: capture the persistent device cookie. Move it to a different browser, different IP, different user agent. If the system still skips MFA, the device-trust binding is bolted to the cookie alone, not to any actual device fingerprint or out-of-band signal.
Mitigation opinion: device trust should be bound to a stable fingerprint (browser characteristics + IP class + behavioural patterns) AND require re-verification on any sharp change. It should NEVER be cookie-only.
7. Sign-up race condition allowing duplicate accounts on the same identifier
Two requests to /signup with the same email arrive in 50ms. Both check "does email exist?" — both get "no." Both INSERT. Now there are two accounts. The system reconciliation logic later does something undefined. I've seen this lead to corporate trial-account abuse, loyalty-points duplication, and free-tier exhaustion.
Test pattern: fire 20 concurrent signup requests with the same identifier. Check if more than one succeeds. Database-level unique constraint is required; "check then insert" is not sufficient.
8. Authentication bypass via response manipulation
OAuth callback returns {"authenticated": true, "user_id": 12345}. The client trusts this. Changing 12345 to 1 in transit logs you in as user 1. I have seen this in 2024. It will happen again in 2027.
Test pattern: intercept every response from auth endpoints. Try to flip the success/failure flag. Try to change the user ID. Try to add or remove fields.
9. Session fixation
Application accepts session cookies from the URL or from any source, doesn't rotate session ID on login. Attacker sets a known session ID in the victim's browser (via XSS, MITM, or social engineering), gets the victim to log in, then uses the same session ID.
Test pattern: set a custom session cookie before login. Log in. Check if the session ID rotated. If not, you have the bug.
10. JWT confusion or signature stripping
Server accepts a JWT with alg: none. Or accepts an RS256 token signed with HS256 using the public key as the HMAC secret. Or trusts JWTs without verifying signature at all because some library quirk.
Test pattern: take a valid JWT. Change alg to none. Remove the signature. Send it. If accepted, you own the system. Variant: change alg from RS256 to HS256 and sign with the public key.
Mitigation opinion: pin the algorithm explicitly on the verification side. Never use a JWT library default. Reject any token with alg you didn't expect.
Section B — Authorisation and access control (11–20)
11. IDOR — Insecure Direct Object Reference (the original)
GET /api/orders/12345 returns order 12345. Changing it to /api/orders/12346 returns somebody else's order. The classic.
Test pattern: create a second test account. Note any object ID associated with your account. Switch to the first account's session, request the second account's object ID. If you get it back, IDOR.
12. IDOR via predictable identifiers
Same as #11, but the application "protects" against IDOR by using UUIDs or hashes for object references. The defence fails because the identifiers are predictable: incrementing integers exposed elsewhere, hashes computed from low-entropy input (md5(user_id + timestamp)), or UUID v1 (which encodes the MAC address and a timestamp).
Test pattern: enumerate 100 IDs from your own account. Inspect them for patterns. Try to predict another user's ID.
13. Authorisation check on the front-end, not the back-end
UI hides the "delete user" button unless current user is admin. The API endpoint DELETE /users/{id} works for any authenticated user because the actual authorisation check is missing on the backend. Pattern is widespread in single-page-app + REST architectures where the dev team forgot that the SPA is just one client among many.
Test pattern: scroll the UI looking for admin-only actions. Use developer tools to find the corresponding API endpoint. Call it from your non-admin account.
14. Privilege escalation via parameter manipulation
POST /api/profile/update with {"name": "Alice", "role": "admin"}. If the role field is accepted, escalation. If is_admin: true works, escalation. If you can pass tenant_id as your own and have it stored on a different user's record, lateral escalation.
Test pattern: mass-assign every plausible attribute. Try role, is_admin, permissions, tenant_id, organisation_id, verified, paid, kyc_status, credit_limit. Whatever your business has, try to set it.
15. Authorisation check that respects only one tenant boundary
Multi-tenant SaaS. User A is in tenant T1. User B is in tenant T2. The application correctly checks "is this user allowed to see this object?" but does not check "is this object owned by the user's tenant?" Result: user A can access user B's data by guessing object IDs.
Test pattern: create two tenants. Create objects in each. Cross-request — try to access tenant T2's objects from tenant T1's session.
16. Vertical privilege escalation through stale role caches
User had admin role. Role was revoked in the database. But the role is cached in the JWT or the session for 24 hours. Effective revocation does not happen until cache expiry.
Test pattern: get an admin token. Have an admin revoke your role. Continue using the token. If you can still execute admin actions, the system has a role-cache freshness bug.
17. Authorisation through obscurity in admin URLs
/admin/dashboard — but only if you "know about it." No real authentication. The defence is that the URL is "private." Spider any application long enough and you find one.
Test pattern: brute-force common admin paths: /admin, /console, /dashboard, /internal, /debug, /.well-known. Check robots.txt, sitemap.xml, JS bundles for references.
18. Permission elevation via group membership change
User joins a group. The application grants the user all permissions of the group at join time, but does not revoke them when the user leaves the group. Or, switching the user's group adds new permissions without removing old ones.
Test pattern: join a high-permission group. Leave it. Verify your permissions match what your current group should give you, not the union of all groups you've ever been in.
19. Forgotten roles in legacy endpoints
System has new RBAC model. Old endpoints still check the old, simpler model. Or some endpoints don't check anything because the developer assumed they were behind the gateway, but the gateway was reconfigured two years ago.
Test pattern: list every endpoint in the API specification. For each, manually verify that the authorisation check is present, current, and tied to the new RBAC. Watch especially for endpoints under /v1/ or /legacy/.
20. Cross-tenant resource enumeration via search
You can't directly GET another tenant's user, but you can call GET /api/users/search?email=* and get hits across all tenants because the search index doesn't filter by tenant.
Test pattern: try every search/list/filter endpoint with broad queries. Test specifically for partial-match queries (LIKE, regex). Check if results include any data from outside your scope.
Section C — Pricing, payments, and economic logic (21–30)
21. Negative quantity in checkout
POST /cart/add { item_id: 123, quantity: -5 }. If accepted, your cart total goes down. Checkout refunds you the negative amount. Yes, this still ships in 2026. I've seen it twice in the last year on Indian e-commerce platforms.
Test pattern: pass negative quantities everywhere — cart, order updates, transfer amounts, withdrawal requests. Pass zero, pass floating-point, pass integer overflows (2^31 - 1).
22. Promo code that can be used by the same user repeatedly
Promo code FIRSTORDER gives ₹500 off. Designed for first orders. Application checks "has this user used FIRSTORDER before?" — but the check is on a flag that gets set after order completion. Cancel the order, the flag rolls back. Order again. Free ₹500. Some platforms let you "reset" the flag through customer service requests.
Test pattern: apply a promo. Cancel before completion. Reapply. Cancel before completion. Loop until you've extracted economic value. Variant: split a single order into multiple to apply the promo to each.
23. Price tampering on the client side
Client sends the item price along with the item ID at checkout. Server trusts the client's price.
Test pattern: send the order with a price of 1 paise. Or 0. Or negative. Or a price from a different currency entirely (USD value sent for an INR item).
Mitigation opinion: clients send IDs, never prices. The server computes the total from canonical, server-side data. If a client ever sends a price, the system is broken.
24. Currency confusion
The application accepts an item ID and a quantity. Backend computes total in INR. The client displays total in USD because the user is in the US. Refunds use the same logic in reverse — but with a different exchange rate, applied to the original transaction date, not the refund date. Net result: refunds either bleed or print money.
Test pattern: place orders across currency boundaries. Refund them. Check the rupee/dollar deltas between the original charge and refund. Anything non-zero is a finding.
25. Refund-after-shipment loophole
Marketplace refunds you upon dispute resolution. The seller still ships the goods because the shipment trigger fires on order placement, not on payment confirmation. Refund + goods = free goods.
Test pattern: place an order. Initiate dispute immediately. If the dispute resolution path runs in parallel with the shipment path, you have the bug. (This often gets caught only when one specific category — high-value, low-volume goods — gets targeted.)
26. Race condition on balance checks
User has ₹100. Submits ₹100 withdrawal at T1. Submits ₹100 withdrawal at T1+5ms. Both pass the "do you have ₹100?" check before either decrements the balance. Both withdrawals succeed. Account goes to -₹100.
Test pattern: blast 50 concurrent withdrawals at exactly the balance amount. Check final state. Variant for transfers, purchases, loyalty point redemption, voucher application.
Mitigation opinion: database transactions with proper isolation level (SERIALIZABLE for financial paths). Alternative: explicit row-level locks. Optimistic concurrency control (compare-and-set) for high-volume cases.
27. Loyalty / cashback / rewards point arbitrage
Earn points on purchase, refund the purchase, keep the points. Earn 2× on category A, transfer to category B, get more cashback. Combine two non-stackable offers because the stacking check ran on offer X before offer Y was applied.
Test pattern: every loyalty mechanism. Refund flow. Stacking. Transfer between accounts. Sibling offer combinations.
28. Coupon code brute-forcing
/api/coupon/validate?code=XXX. No rate limit, no user binding, no captcha. Iterate 8-character alphanumeric codes until you find a working one.
Test pattern: send 10,000 codes. Look for differential responses (valid: "20% off", invalid: "code not found"). Even without finding valid codes, identifying the rate-limit absence is the finding.
29. Currency precision rounding exploits
Application rounds to 2 decimal places. Place 1000 orders of ₹0.004 each. Each rounds to ₹0.00 (free). But the loyalty engine credits ₹0.004 each, retained at full precision. After 1000 orders, you have ₹4 in points.
Test pattern: micro-transactions. Examine where rounding happens. If charges round down but credits round up, or vice versa, the system bleeds.
30. Free trial loop
Sign up. Free trial for 30 days. Cancel before expiry. Sign up again with the same payment method but a different email. Loop. Some systems try to detect this via device fingerprinting; most fail.
Test pattern: signup → trial → cancel → signup with email+1@domain.com. Check whether the system detects the abuse. Most don't.
Section D — Workflow and state machine flaws (31–40)
31. Skipping mandatory workflow steps
Onboarding has steps 1 → 2 → 3 → 4. Step 3 is KYC. Each step navigates to the next via a URL change. Just type the step 4 URL directly. Onboarding completes without KYC.
Test pattern: identify any wizard, multi-step form, or sequential workflow. Try to jump to the final step. If the application doesn't validate that all prior steps completed correctly server-side, you have a workflow bypass.
32. Replaying a one-time action
User confirms a 2FA challenge by submitting a code. The same code is accepted again, hours later. The server should bind the code to a single use AND a short window.
Test pattern: capture any confirmation request — 2FA, email verification, transaction OTP. Replay. If accepted, the action is replay-able.
33. State transitions that move backwards illegally
Order has states: created → paid → shipped → delivered. The system has a PUT /order/{id}/state endpoint. Set the state to paid for an order that's already shipped. Now the system shipped the goods, considers it paid, but the customer won't be charged because the payment hook fires on entering the paid state and it's already past that point.
Test pattern: every state machine in the application. Try every illegal transition. The bug isn't always exploitable, but the existence of the bug is the finding.
34. Concurrent state mutations
Two parallel requests modify the same entity. Last-write-wins overwrites intermediate decisions. Risk approval and risk rejection arriving in the wrong order leaves a rejected loan disbursed.
Test pattern: fire pairs of conflicting state changes concurrently. Inspect final state. If it's deterministic, that's good. If it depends on timing, that's a bug.
35. Resumable workflows that resume in privileged state
Long-running workflow saves state to a database. Workflow can be resumed from saved state via "resume token." The token decodes to a workflow ID and current step. Modify the current step. Resume into an advanced step you weren't supposed to reach.
Test pattern: capture a resume URL or token. Tamper with the step indicator. Resume.
36. Permission grant that lingers after revocation
User invites collaborator. Collaborator accepts. Owner revokes the invite from settings. Collaborator's access continues because the revocation only deletes the invite record, not the access record that was created on acceptance.
Test pattern: every revocation path. Verify the actual access is removed, not just the invite/request artefact.
37. Workflow timeouts that fail open
Workflow waits for approval from manager. Manager has 7 days. If no response in 7 days, what happens? In secure systems: it expires and requires re-request. In not-secure systems: it auto-approves to "avoid blocking the user." If you can identify auto-approval, target it.
Test pattern: identify approval workflows. Wait for the timeout. Observe the default behaviour.
38. Cascading deletes that don't cascade
Delete user account. The deletion removes the user record but leaves their orders, their files, their messages, their billing history, and their support tickets. Privacy violation under DPDPA, GDPR. Security risk if any artefact contains credentials.
Test pattern: delete a test account. Audit every dependent system for orphaned records.
39. Cancellation that leaves systems out of sync
Cancel subscription on Stripe. Application doesn't get the webhook in time. Application thinks user is still subscribed. Until next reconciliation job, the user has access to features they cancelled. Variant: cancel succeeds on application, but Stripe wasn't notified. User is double-charged.
Test pattern: trigger cancellation. Examine all downstream systems within 1 second, 10 seconds, 1 minute. Look for divergence.
40. Idempotency key reuse across distinct operations
Stripe-style idempotency: send a key, the server dedupes. Same key for "charge ₹100" and "charge ₹1000" — second one returns the first's response. Customer thinks they paid ₹1000; they paid ₹100. Or vice versa.
Test pattern: send distinct operations with the same idempotency key. The system should reject or return the original response with a warning. If it silently aliases, that's the bug.
Section E — Input validation and trust boundaries (41–50)
41. Trusting the user's email after self-signup
Signup with attacker@victim.com. Application sends a verification email to that address. Attacker doesn't have access to it. Application then auto-verifies based on a JavaScript check on the client, or based on the user clicking a button labelled "I've verified."
Test pattern: signup with an email you don't control. Look for any path that confirms the email without you actually receiving and clicking a verification link.
42. Header injection in customer-facing emails
Customer enters their name as Alice\r\nBcc: attacker@evil.com. The application uses this in an email header. Now every "thanks for your order" email also goes to the attacker, including order details.
Test pattern: inject CR/LF into every field that might end up in an email, header, or HTTP response. Look for boundary-breaking behaviour.
43. File upload extension bypass
Application restricts uploads to .jpg/.png. Server checks the extension only. Upload shell.php.jpg. Webserver routes anything with .php to the interpreter regardless of trailing extensions.
Test pattern: try every variant: double extensions, null-byte injection (shell.php\x00.jpg), MIME type mismatch (PHP file with image/jpeg content-type), polyglot files (valid JPEG with embedded executable payload).
44. SSRF via metadata, redirects, or DNS
Application accepts a URL for "preview this image." Send http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS metadata). Or http://localhost:6379 (internal Redis). Or http://attacker.com/redirect which redirects to an internal target.
Test pattern: every endpoint that fetches a URL. Send AWS/GCP/Azure metadata URLs. Send localhost variants (127.0.0.1, ::1, 0.0.0.0). Send DNS-rebound hostnames.
45. Image / file rendering that executes content
PDF uploads accepted. PDF contains JavaScript that, when rendered in a viewer, exfiltrates the viewer's session cookies. SVG uploads accepted. SVG contains a <script> tag. The "view image" page renders the SVG inline as HTML.
Test pattern: upload PDF with embedded JS, SVG with <script>, HTML with <iframe>, malformed images with EXIF metadata containing exploit payloads.
46. Mass assignment
PUT /user/profile { name, email, profile_pic } accepted. But the model also has is_admin, account_balance, tenant_id. Send all of them. The framework binds the request body to the model and the unprotected attributes get overwritten.
Test pattern: enumerate every attribute on every model (via API documentation, error messages, or source code if available). For each, try to set it via the public PUT/PATCH endpoint.
47. XML external entities (XXE)
Application accepts XML. XML parser is not configured to disable external entities. Send an XML document that references file:///etc/passwd or http://internal-service/secret.
Test pattern: every XML, SOAP, or RSS-consuming endpoint. Send XXE payloads.
48. Server-side template injection (SSTI)
User-controllable input is rendered through a templating engine without proper escaping or sandbox. {{7*7}} returns 49. Now you have remote code execution.
Test pattern: try template syntax for every common engine in any input that might be rendered: Jinja2 ({{}}), Twig ({{}} and {%}), Mustache ({{}}), Velocity ($), Freemarker (${}).
49. Open redirect
GET /redirect?url=https://yoursite.com/login redirects to the URL. Change to https://yoursite.com.evil.com/phishing and the redirect goes to the attacker. Useful in OAuth flow hijacking and phishing.
Test pattern: every redirect-bearing parameter. Test with external URLs, with relative-vs-absolute parsing tricks (//evil.com, \\evil.com, https:evil.com).
50. HTTP request smuggling
Front-end and back-end disagree about request boundaries. Front-end uses Content-Length, back-end uses Transfer-Encoding. Send a request with both, conflicting. Front-end forwards what it sees, back-end sees a different request inside, and you've smuggled a privileged request.
Test pattern: send requests with both Content-Length and Transfer-Encoding: chunked. With varied capitalisation. With leading whitespace. If the system responds inconsistently, smuggling is possible.
How to actually use this list
Three modes:
As a pentest checklist. Print it. Go through each item in your test plan. Mark each as Tested / Found / Not Applicable. The discipline of going through every one catches bugs you would otherwise skip because they "didn't look likely."
As a code review prompt. For each item, search the codebase for the corresponding patterns. Authentication endpoints, authorisation middleware, payment paths, state transitions, input validation. Many of these bugs leave architectural fingerprints — find those fingerprints and you find the bug.
As a dev team briefing. Translate each item into a positive requirement and add it to your security definition-of-done. Item 26 becomes: "Concurrent operations on the same balance must use database-level row locks." Item 23 becomes: "Server computes order totals from canonical pricing; client never sends prices." Convert the negative ("don't do this") into a positive ("do this") and your developers can actually act on it.
If a finding from this list lands in your next pentest report, it's mostly because somebody didn't test for it. The bugs are usually old. The exploitability is usually high. The fix is usually small. The difficulty is the test design — the things you have to think of trying.
A note on what this guide deliberately omits
No SQL injection. No XSS. No CSRF (mostly). No buffer overflows. No deserialisation gadget chains. These are tracked in OWASP and CWE and covered well elsewhere. The fifty above are the patterns that scanners miss and that I keep seeing exploited in production.
Also missing: cloud-misconfiguration patterns (S3 public buckets, exposed IAM roles, debug endpoints in production), supply-chain attacks (compromised dependencies, typo-squatted packages), and infrastructure-level attacks. Those belong in adjacent references.
What I would add if I were rewriting this in 2027
AI / LLM-integrated apps have introduced their own class of business logic bugs that I'm tracking but haven't yet seen enough production incidents on to commit to a curated list. Watch for: prompt injection that escapes RAG retrieval boundaries; tool-call confusion where an LLM agent calls an authorised tool on behalf of an unauthorised user; reasoning-step injection where the user's input alters the agent's "internal" reasoning; over-trust of LLM output that ends up in privileged execution paths. By the next version of this guide, those will have their own section.
This guide is a practitioner reference for application security engineers, pentesters, and developers. It draws on production incident retrospectives and pentest engagement patterns. It is not a comprehensive vulnerability taxonomy — for that, see CWE and OWASP. Findings here are tactical: what to test for, in what order, with what expected outcomes.
ControlForge maps application security and secure SDLC controls across ISO 27001, SOC 2, NIST CSF, PCI DSS, RBI CSF, and other frameworks. The synthesis catalogue includes clusters on secure development, code review, application security testing, and vulnerability management. Available at controlforge.com.