VAPT.UK is led by Hassan Jawaid, who has reported 1,000+ vulnerabilities across 100+ bug bounty programmes, and 56+ organisations have put his name in a hall of fame for it. Very few of them needed anything clever. The most common vulnerabilities in SaaS applications are not exotic. The one at the top of the OWASP API list below, and the one to look for first in any SaaS test, is IDOR: one user reading a record that was never theirs. The rest are the same handful of mistakes, made by different teams. The same operator now tests UK SaaS under a signed letter, and the patterns do not change when the invoice is in pounds sterling.

Nearly every one of those patterns traces back to one of four assumptions. Name the assumption and a team can usually find the bug class on its own. So: eight patterns under four root causes — what each looks like, why it keeps shipping, how to look yourself, which testing finds it. Where we say a class is common, that is impression, not a count. Every self-check below is for a product you own or are authorised in writing to test. Against a system you do not own and have no authorisation to test, sending those requests is an offence under the Computer Misuse Act 1990. We take that authorisation in writing before a packet is sent, and so should you.

The most common vulnerabilities in SaaS applications, by the public data

The counts give one answer. OWASP moved broken access control to the top of the Top 10 in 2021, up from fifth, with more occurrences in the contributed dataset than any other category — over 318,000 — and an average incidence rate of 3.81%. The 2025 edition keeps it at number one. The OWASP API Top 10 (2023) puts authorisation failures at three of its top five: API1 broken object level authorisation, API3 broken object property level authorisation, API5 broken function level authorisation.

That matches what we see. Not injection. Not a 2014 SQLi. An identifier that was trusted because it arrived in the request.

Root cause one: trusting the client

The server receives a request and believes it. The identifier in the path, the fields in the body, the price in the form. The client stops being yours once it leaves your CDN.

1. BOLA / IDOR: the identifier is the authorisation

What it looks like. An invoicing feature. A GET to /api/invoices/48213/pdf returns a PDF. Change 48213 to 48212 and you have someone else’s invoice. The same shape lives in exports, attachments and the “download your data” endpoint built for UK GDPR subject access requests.

Why it keeps shipping. The ORM makes findById one line and findByIdForUser a design decision. Authorisation lives in the controller, where it is easy to forget, rather than in the data layer, where it is far harder to forget. The test suite runs as one user, so the test that would catch it is never written.

Look for it yourself. Two accounts, same role. Log every request account A makes in a day of use, replay each with account B’s session, diff the responses. Anything that is not a 403 or an empty result needs an explanation. Some may not have one. Bugcrowd’s Vulnerability Rating Taxonomy puts an IDOR that both reads and changes sensitive data at P1 behind an iterable identifier, P3 if it only reads, and P4 behind a UUID — lower, still a finding.

What finds it. A manual API security test with two accounts at the same privilege level, plus one per role.

2. Mass assignment: the body decides what the model becomes

What it looks like. A profile update endpoint accepts a name field. It also accepts role, plan and credit_balance, because the model has those columns. OWASP folded mass assignment into API3:2023, broken object property level authorisation.

Why it keeps shipping. Frameworks bind request bodies to models because it is convenient, and the allow-list is the step the tutorial skipped. Nobody re-reads an allow-list that never existed.

Look for it yourself. For every write endpoint, list the fields the UI sends and the fields the model accepts. If a GET returns is_admin, assume someone will try to PUT it back. Send it, then check the database, not only the response. Do it for nested objects too — the organisation object inside the user object is where privilege lives. Allow-lists per endpoint, per role. Not a deny-list.

What finds it. The same two-account API test, field by field, with the schema in hand. Without a schema it costs a day and slightly reduces coverage confidence.

Root cause two: confusing authentication with authorisation

Authentication answers who you are. Authorisation answers what you may do. Most products get the first right and leave the second to the user interface.

3. Function-level authorisation and the hidden admin

What it looks like. The admin menu is not rendered for ordinary users, so they never see /admin/users/export. The route still exists and answers anyone with a session. OWASP files guessed admin endpoints and swapped HTTP verbs under API5: easy to exploit, easy to detect.

Why it keeps shipping. The front end is the only client anyone imagined, so hiding a button feels like access control. Authentication middleware is global — every route needs a session — while the authorisation check is per route, written by hand, and the newest endpoint is the one nobody copied the check onto.

Look for it yourself. Dump the router. Not the navigation, the router — every path and method the framework will answer. For each, write down who may call it. Search the JavaScript bundle for paths you have not seen in use, sign in as the lowest role you have, and call each one directly. Change the HTTP verb: GET to PUT, PUT to DELETE.

What finds it. A manual web application test that builds a model of every role and every route from the server, not from the screens.

4. Multi-tenant isolation

What it looks like. Tenant A reads tenant B’s records by changing an organisation identifier they were never meant to know. The session says which tenant you are. The query filters by the org_id in the request instead.

Why it keeps shipping. Tenancy was added after the first customer, so tenant_id is a column rather than a boundary. The ORM’s default scope enforces it everywhere until one background job needs to run across all tenants, switches the scope off, and nobody switches it back on for the next job that copied the pattern.

Look for it yourself. Two tenants, one user each, same role. Every endpoint that takes any identifier gets the other tenant’s identifier. Then the places that are not endpoints: search, the cache key, and the asynchronous paths — emailed reports, scheduled exports, webhooks — because those run outside the request that carried your session.

What finds it. Web or API testing with two tenants in the authorisation letter, not only two users. If you can supply only one account per role, we name which findings become unprovable before anything is signed. If a cross-tenant read of personal data ever becomes an incident, it is also what the ICO reads afterwards: whether you knew the class existed and whether you had tested for it.

5. Authentication, sessions and tokens

What it looks like. A password reset token that is a timestamp, or one that still works after use. A reset link built from the Host header, so the attacker’s domain receives it. A JWT where the server accepts alg none, or verifies an RS256 token using the public key as an HMAC secret. MFA enforced on the login page and not on the API the login page calls.

Why it keeps shipping. Authentication is written once, early, by whoever was there, and then nobody touches it because the blast radius of a mistake is total. Library defaults are permissive in ways the documentation mentions on page four. The mobile app needed its own token endpoint “for now”, and for now has lasted three years.

Look for it yourself. Request two password resets and see whether the first token still works. Change the Host header on the reset request and read where the link points. Decode your own JWT and ask what the server does if you change the role claim and the algorithm. Log out and replay the last authenticated request.

What finds it. Authentication and session is a named coverage area of every web engagement we run — MFA bypass, OAuth / SSO, JWT.

Root cause three: assuming the workflow

The sequence of screens lives in the front end. The server checks each request on its own, assuming the previous one happened, in order, once.

6. Business logic, state and race conditions

What it looks like. A checkout where payment is confirmed at step three and the order is created at step four by a request that needs only the order ID. A voucher that is single-use until two requests arrive in the same hundred milliseconds. Business logic vulnerabilities do not look like bugs. The code is doing exactly what it was told. HackerOne has written that race conditions are a very small fraction of its platform reports, but in a payments flow one is worth more to an attacker than ten reflected XSS findings.

Why it keeps shipping. The state machine is drawn once, on a whiteboard, and then implemented as a series of screens. The server never gets the diagram. Tests are sequential by construction. A test for “two at once” has to be written by someone who already suspects the problem.

Look for it yourself. Write down the five rules your business depends on. One voucher per account. Refunds never exceed the charge. For each, try changing the order, repeating a step, running two steps at once, and editing any value the client sends that the server could have calculated itself.

What finds it. Manual testing with the application model shared before exploitation starts. Ten minutes with the product owner, correcting that map, saves a day of guessing at the rules.

Root cause four: forgetting what you shipped

Not a coding mistake. An inventory problem. Everything a product has ever exposed is still exposed until somebody removes it, and removal has no sprint ticket.

7. The forgotten surface: old versions, staging and deprecated routes

What it looks like. /v2 has the new authorisation model. /v1 is still answering, with the old one. A staging host on public DNS with last quarter’s production database and a shared password in a wiki. OWASP keeps this on the API list — Improper Assets Management in 2019, Improper Inventory Management in 2023 — and names deprecated API versions and exposed debug endpoints as what an inventory is meant to prevent.

Why it keeps shipping. You cannot delete what a customer might depend on, and the team that knew which customers used /v1 has moved on. Shipping a feature has a ticket, a reviewer and a demo. Removing one has none of those. Staging is nobody’s.

Look for it yourself. Search certificate transparency logs for your domains. Check whether staging accepts a production credential. Ask who last deployed each host. If the answer is a person who has left, that host is in scope.

What finds it. Reconnaissance, which is step two of every engagement we run and the map you receive before we break anything. For the perimeter as a whole it is external network testing.

8. What the client keeps, and what the server trusts it with

What it looks like. On mobile: a session token in plaintext preferences, an API key in the binary. More important than either: the app enforces the role and the API does not. On desktop: a .NET or Electron line-of-business client that connects straight to the database with one shared credential for every customer, or an updater that runs as SYSTEM and fetches over HTTP.

Why it keeps shipping. A compiled thing in the customer’s hands feels private. It is not. Mobile is often built by an agency against an API designed for the web app, and nobody re-examines the backend for a client that can be decompiled in an afternoon. The desktop client was written for a trusted LAN, and the LAN stopped being trusted without anyone telling the application.

Look for it yourself. Unpack your own app from the store and search it for keys, URLs and the word “secret”. Then set the app aside and call the API directly as a low-privilege user. For desktop, install on a clean VM as a standard user, run the updater, and look at what that user can now write to.

What finds it. A mobile engagement that includes the backend API — a mobile test that stops at the binary is theatre — and a desktop and thick-client test that covers the installer, the IPC and the server behind the client.

What the pattern says about buying testing

Most of the eight are decisions, not defects, and a scanner sees almost none of them. Cyber Essentials Plus does not look either. It verifies five control themes against a sample and does not test your own application’s logic. Both are worth having. Neither is this.

So buy testing by surface and root cause, not by day count. Two accounts at the same privilege level, one per role, one that should see nothing, two tenants. Ask for a coverage statement that names the endpoints that were not tested. Ask whether the re-test is in the fee. Ask how the proof is bounded, because a tester who dumps your customer table to prove an IDOR has caused the incident they were hired to prevent. If the answer to any of those is a logo, keep looking. The services page says which engagement fits which situation.

And do the self-checks first. None of them needs a tester, and all of them are worth doing before the scoping call. If they show you need less than you expected, we will say so. A manual test adds the second tenant, the hundred requests you did not think to send, and a report a customer’s pentester can replay line by line.

Then fix the root cause, not the finding. Move authorisation into the data layer. Allow-list request bodies. Give the router an owner. Turn /v1 off. Each closes a class rather than a ticket, and the next report — ours or a stranger’s — gets shorter.

If you want this run against your own product rather than read about it, the scoping call is thirty minutes. Assets, roles, tenants. A quote in pounds sterling. Written CMA 1990 authorisation before a packet is sent.

Questions

Questions this usually raises

Broken access control — and its API form, broken object level authorisation, better known as IDOR — leads the OWASP Top 10 for 2021 and 2025 and the OWASP API Top 10 for 2023. The bug bounty platforms put the same class at or near the top of what gets reported: Bugcrowd’s 2023 platform data had IDOR and broken access control second only to reflected XSS, and HackerOne’s 2025 report has valid IDOR reports rising again year on year. After that: mass assignment, function-level authorisation and hidden admin routes, tenant isolation, password-reset and token handling, business-logic and race conditions, and forgotten API versions. For a multi-tenant product, test cross-tenant reads first.
They are the same flaw with two names. IDOR, insecure direct object reference, is the older web term. Broken object level authorisation, BOLA, is the name the OWASP API Top 10 uses, where it sits at number one. Both describe a server that treats the identifier in a request as the authorisation decision instead of checking whether the person asking is entitled to that object.
No. A UUID makes the identifier harder to guess, which lowers the severity but does not remove the flaw. The identifier still leaks through notification emails, shared links, exports and webhook payloads, and anyone who has been sent one holds a valid key. The fix is to check entitlement where the query runs, not to lengthen the identifier.
Mostly not. A scanner can find a missing header, a known CVE and some injection. It has no idea which user should see which invoice, which tenant owns which record, or which step of a checkout was supposed to come first. Authorisation and business logic testing needs two accounts, a model of the roles and a person deciding what should have been refused. Scanners support an engagement. They do not replace one.
A system you own and operate is yours to authorise. The harder cases are the ones you do not own: a supplier’s API, a third-party service your product calls, your hosting provider’s infrastructure. Testing those without the owner’s authorisation is an offence under the Computer Misuse Act 1990. Get that authorisation in writing. We will not test without it. Where a third party hosts an asset, confirm you are entitled to authorise testing of it before a packet is sent, and read the provider’s published testing policy.
A focused web application takes three to five working days of testing. A typical product API takes four to seven, driven by endpoints, roles and tenants rather than request volume. Reporting adds two to three days, with a draft within two to three working days of the window closing and a walkthrough call before the final document. Start is usually within two to three weeks. The critical path is almost always test accounts and the signed authorisation letter.

Need this as an engagement, not an article?

UK hours. GBP quote. Written CMA 1990 authorisation.

Request a pentest