The OWASP API Security Top 10 (2023) Testing Checklist

Aug. 28, 2026
owasp-checklist

I have spent the better part of a year intentionally breaking APIs, and every time, I was reaching for the same list in my head: the OWASP API Security Top 10. And every time, I wished I had one page that told me precisely what to check for each of the ten, both as the person attacking the API and as the person trying to prove theirs is safe.

So this is that page.

For every item in the OWASP API Security Top 10 2023, you get two tracks. 'How I test for it' is the attacker's view: the requests to send, the things to swap, the tools to reach for, and what a positive finding looks like. 'How to prove you are covered' is the defender's view: the controls to verify so you can say, with evidence, that this class of flaw does not apply to you.

A disclaimer before anything else

Working through this checklist does not guarantee your API is secure. Nothing does. A checklist is a floor, not a ceiling. It catches the ten most common classes of failure, but a determined attacker chains findings together in ways no list anticipates, and business logic flaws in particular live in the gaps between categories. Treat this as a strong starting point and a way to make sure you have not missed the obvious, not as a certificate of safety.

With that said, let's get into it.

How to use this checklist

Two things are worth setting up before you start, because almost every test below depends on them.

  • You need at least two accounts: A victim and an attacker, ideally a third that is an admin. Most of the authorization tests are impossible with a single account, because the whole question is: "Can user A reach user B's stuff?" If you only take one thing from this article, take this
  • You need an intercepting proxy in front of a browser or client: Burp Suite or mitmproxy, with FoxyProxy to switch it on. If you have never set that up, I wrote a full walkthrough in My Lab Setup for API Security Testing, and a piece on reconstructing an API spec from traffic when the target has no documentation, which is where a lot of engagements begin.

Here is the whole list at a glance before we go deep on each

OWASP API Testing Checklist
# Category The one-line question Primary tools
API1 Broken Object Level Authorization Can I reach another user's resources?? Burp Repeater, Autorize, ffuf
API2 Broken Authentication Can I break, forge, or brute force my way into an account? xjwt.io, jwt_tool, Burp Intruder, hashcat
API3 Broken Object Property Level Authorization Does it leak fields I should not see, or accept fields I should not set? Burp Repeater, Param Miner
API4 Unrestricted Resource Consumption Can I make it do expensive things without limit? Burp Turbo Intruder, ffuf
API5 Broken Function Level Authorization Can a normal user reach an admin function? Burp Repeater, Autorize
API6 Unrestricted Access to Sensitive Business Flows Can I automate a business flow past its intended limit? Burp Intruder, custom scripts
API7 Server Side Request Forgery Can I make the server fetch a URL I control? Burp Collaborator, interactsh
API8 Security Misconfiguration Are the headers, methods, errors, and TLS locked down? nikto, nuclei, testssl.sh
API9 Improper Inventory Management Are there old versions, staging hosts, or shadow endpoints live? ffuf, subfinder, kiterunner
API10 Unsafe Consumption of APIs Does it blindly trust data from the APIs it consumes? mitmproxy, code review
01-request-lifecycle 1

API1:2023 - Broken Object Level Authorization

This is number one for a reason. It is the flaw I have found in almost every single lab I have touched, and it is the simplest to understand. The API receives an object ID from the client and uses it to fetch a record, but it never checks whether the requester is authorized to access that record. The ID is trusted as if possessing it were equivalent to being authorized to use it.

Is it vulnerable?

Ask these, straight from the OWASP criteria:

  • Does an endpoint take an object ID (in the path, query, body, or a GraphQL node) and act on that object?
  • Is the only check a comparison against a client-supplied value, rather than a server-side ownership check?
  • Do random, unguessable IDs serve as the security control instead of real authorization?

How I test for it

The core move is the two-account swap

  1. Create user A and user B. As user B, create an object and note its ID.
  2. Log in as user A, capture a request to one of A's own objects in Burp Repeater.
  3. Swap A's object ID for B's, keeping A's token. Send it.
  4. If you get B's data back with a 200 OK where you expected a 403, that is Broken Object Level Authorization.

Beyond the manual swap:

  • Enumerate sequential IDs with Burp Intruder or ffuf over the ID position. A run of 200 OKs returning different users' data is the finding.
  • Attack unguessable IDs too. A GUID is not authorization. Harvest IDs that leak in other responses, logs, or referrer headers, then replay them.
  • Test every verb and location. A read might be blocked while PUT or DELETE on the same object is not. Try the ID in the path, in the body, wrapped in an array, in JSON, and as form-encoded data.
  • Blind BOLA counts. Even with no data echoed back, a successful DELETE or PUT against another user's object is a confirmed finding.

02-api1-bola-sequence 1

In the Vulnerable Bank API article I wrote, I swapped an account number in GET /transactions/{account_number} while sending my attacker token, expected a 403, and got the victim's entire transaction ledger

In Zero-Health, the same GET /api/lab-results/:id endpoint returned another patient's medical results.

In VAmPI, I pulled another user's private book by name

How to prove you are covered

  • A centralized authorization layer enforces per-object ownership on every endpoint that takes an object ID, not on a per-handler basis.
  • The check compares the authenticated user against the object's true ownership record on the server side. Never against a client-supplied ID.
  • Multi-tenant queries are scoped in the data layer, for example, WHERE owner_id =:session_user, so isolation holds even if a check is forgotten.
  • Record IDs are random GUIDs, treated as a defense-in-depth measure and never as the only control.
  • Automated authorization tests run in CI, with positive and negative cases per role, and block the deploy if they fail.

Reference: API1:2023 Broken Object Level Authorization

API2:2023 - Broken Authentication

Authentication is exposed to everyone, making it the most poked part of any API. When it breaks, the attacker does not read one user's data; they become that user.

Is it vulnerable?

  • Does the login allow credential stuffing or brute-force attacks with no lockout or captcha?
  • Are tokens or passwords ever sent in the URL?
  • Can a user change email, password, or MFA settings without re-entering their current password?
  • Does the API accept unsigned JWTs (alg: none), fail to verify the signature, or skip the expiry check?
  • Are passwords stored in plaintext or weakly hashed, or signed with a weak key?

How I test for it

Authentication has the widest attack surface of the ten, and JWTs are where I spend most of my time. My go-to for the manipulation itself is xjwt.io, where I decode a token, run the dictionary attack against the secret, edit the claims, and re-sign, all in one place.

  • Crack the JWT secret: If the token uses HS256, run a dictionary attack against the signature in xjwt.io, or with jwt_tool or hashcat -m 16500. If you recover the secret, you can forge any identity. This methodology worked in VUln-Bank, Vampi, and Zero-Health APIs.
  • Try alg: none: Strip the signature, set the header algorithm to none, and see if the server accepts an unsigned token.
  • Tamper without re-signing: Change a claim like role or sub and leave the signature. If it is accepted, the server is calling decode() instead of verify().
  • Replay an expired token: If it still works, exp is not being validated.
  • Credential stuff and enumerate users: Run Burp Intruder or ffuf against POST /login. Differential error messages ("username does not exist" versus "password is not correct") hand you a valid-username list, which I confirmed in VAmPI.
  • Attack the sensitive-operation gap: Try changing the email or password without confirming the current password. Combined with a stolen token, that is account takeover.
  • Treat forgot-password as a login endpoint: It rarely gets the same protections. In the Vulnerable Bank, I downgraded /api/v3/reset-password to the legacy /api/v1/forgot-password, whose debug response handed me the reset PIN.

I documented the full scope of JWT attacks and their fixes in "JSON Web Tokens: Anatomy of a Break and Fix

03-api2-jwt-decision-tree

How to prove you are covered

  • Standard, vetted auth libraries. No hand-rolled token or password crypto.
  • Passwords hashed with bcrypt, scrypt, or Argon2, with breached-password checks.
  • JWTs signed with a strong algorithm and key. The server verifies the signature, rejects alg: none, and validates exp, iss, and aud
  • Anti-automation on both login and forgot-password: lockout, captcha, and brute-force protection stricter than the general rate limit.
  • Re-authentication required for email, password, and MFA changes. MFA available where possible.
  • No secrets in URLs. Tokens are invalidated on logout and rotated on password change.

Reference: API2:2023 Broken Authentication

API3:2023 - Broken Object Property Level Authorization

This one merges two older flaws, so it has two sides. The read side is 'excessive data exposure': the API hands back object fields the user should never see. The write side is 'mass assignment': the API accepts fields the user should never be able to set.

Is it vulnerable?

  • Does an endpoint return object properties that the user should not be able to read?
  • Does it allow a user to change, add to, or delete a sensitive property that they should not control?
  • Does it rely on the frontend to hide fields, or use generic serializers like to_json()?

How I test for it

Read side, hunt for exposure:

  • Read the raw JSON in Burp, not what the UI renders. Look for the password, role, is_admin, other users' email or phone numbers, pricing, tokens, and internal IDs
  • Fetch the same object as an admin and a low-priv user, then differentiate the fields

Write side, inject properties:

  • Take a legitimate register or update request and add sensitive fields the UI never exposes: "is_admin": true, "role": "admin", "credit": 50000, "balance": 999999
  • Re-fetch the object to confirm the injected value persisted.

I found the full range of this across labs:

In VAmPI, I added "admin": true to the register call

In vAPI, I discovered a hidden credit field via GET /user/me and then set it to 50000 on registration.

In the Vulnerable Bank, I injected "is_admin": true at register and watched my new token come back with the admin claim set.

On the read side, VAmPI's /users/v1/_debug endpoint exposed every user's plaintext passwords, and the Vulnerable Bank's GET /api/virtual-cards endpoint returned full, unmasked card numbers, expiry dates, and CVV codes because the masking lived only in the frontend.

04-api3-bopla-read-write

How to prove you are covered

  • Responses are built from explicit output DTOs that cherry-pick allowed fields. No entity-wide serialization.
  • A schema validation layer enforces which fields each endpoint may return.
  • Updates use an allowlist of client-writable properties. Server-controlled fields (price, role, ownership, status) are never bindable from the request.
  • Sensitive fields are gated by the caller's role, not just object ownership.
  • Tests cover both sides: sensitive fields are absent for unauthorized roles, and injected privileged fields are rejected.

Reference: API3:2023 Broken Object Property Level Authorization

API4:2023 - Unrestricted Resource Consumption

Every request costs something: CPU, memory, bandwidth, or literal money when an endpoint sends an SMS or calls a paid third party. When there is no limit on how often you can trigger that cost, you have this flaw. It shows up as a denial-of-service attack and as a surprise five-figure cloud bill.

Is it vulnerable?

Any of these limits missing or set wrong makes it vulnerable: execution timeouts, max memory, max upload size, number of operations per request (GraphQL batching), records returned per page, and third-party spending limits.

How I test for it

  • Map the expensive endpoints first: anything that sends SMS or email, generates files, calls a paid service, accepts uploads, or takes a pagination parameter.
  • Probe the rate limit. Send a request rapidly with Burp Repeater's parallel send, Turbo Intruder, or ffuf. No 429 on an auth, OTP, or paid endpoint is a finding.
  • Try to bypass a weak limit. Rotate X-Forwarded-For, change path casing, add a trailing slash, and rotate tokens. If any of these restores unlimited throughput, that is the finding.
  • Brute force small secret spaces. When there is no limit, a short OTP or PIN is quickly exhausted. In vAPI, I generated the whole 4-digit space with seq -w 0 9999 > otp.txt and fuzzed the verify endpoint with ffuf until success: true.
  • Abuse pagination and batching. Set a limit on a huge number, or send an array of many operations in one GraphQL request.

In the Vulnerable Bank Part 2, I hit the password-reset endpoint 100 times with Burp Intruder and got a 200 every single time, never a 429.

How to prove you are covered

  • Container or serverless resource caps on memory, CPU, and restarts.
  • Rate limiting is enforced globally and per endpoint, is stricter on auth and paid actions, is keyed to user and IP, and is resistant to header-spoof bypass.
  • Per-operation throttling, for example, a cap on OTP validations.
  • Max string length, array size, nesting depth, and upload size are enforced on the server side.
  • Pagination is bounded by a hard maximum page size. GraphQL depth and batching limits.
  • Spending limits or billing alerts on every paid integration.

Reference: API4:2023 Unrestricted Resource Consumption

API5:2023 - Broken Function Level Authorization

Where API1 is about reaching another user's data, this is about reaching another role's functions. A regular user calling an admin-only endpoint. The classic tell is an admin action that works with a normal token.

Is it vulnerable?

  • Can a regular user hit an administrative endpoint?
  • Can a user perform a sensitive action just by changing the HTTP method, say GET to DELETE?
  • Can a user from one group reach another group's function by guessing the URL, like /api/v1/users/export_all?

OWASP is blunt about one thing here: do not assume an endpoint is admin-only or regular based on the URL path alone.

How I test for it

  1. Get a low-privilege account and, if you can, an admin one for reference.
  2. Take an admin request and replay it with the low-priv token. A success is the finding.
  3. Try method tampering. If you can GET a resource, try POST, PUT, DELETE on the same path.
  4. Guess admin routes: /admin, /internal, /v1/users/all, /export_all. Test each with the low-priv token.
  5. In GraphQL, call admin mutations as a normal user.

In VAmPI, I reset the admin's password as a normal user with PUT /users/v1/Mel/password, got a 204, and logged in as the admin.

In vAPI, I changed /user/{id} to the collection endpoint /users and got the full user list with a non-admin token.

And a note on negative results, because they matter just as much.

In the Vulnerable Bank, I tried POST /admin/create_admin and POST /admin/delete_account as a standard user and got a clean 403 Forbidden both times. I documented it as not vulnerable. Proving a control works is a finding too.

05-api5-bfla-matrix

How to prove you are covered

  • Deny by default. Every function requires an explicit role grant.
  • Admin controllers inherit from an admin base that enforces role checks. Admin functions within regular controllers also check the role.
  • Authorization is enforced on every HTTP method, not just GET.
  • Checks are role-based and server-side; they are never inferred from the URL path or a client-supplied role field.
  • The full route inventory, including legacy and versioned routes, is reviewed against the role matrix.

Reference: API5:2023 Broken Function Level Authorization

API6:2023 - Unrestricted Access to Sensitive Business Flows

This one is different from the rest, because the individual request is not a bug. The flaw is that a flow that is fine at human speed becomes harmful at machine speed, and nothing stops the automation. One purchase, one reservation, one sign-up is normal. Thousands of them, driven by a script, in seconds, is the exploit.

Is it vulnerable?

An endpoint is vulnerable if it exposes a sensitive business flow without restricting excessive access. Think buying stock (scalping), posting (spam), reserving slots (denial of availability), or creating accounts for referral credit. The risk is business-specific, so you need to understand what the business stands to lose.

How I test for it

  • Recon the flows where volume equals value: checkout, reservations, coupon or referral redemption, account creation, voting.
  • Confirm the flow completes entirely via the API, with no mandatory human step. Replay the entire sequence using only the API calls.
  • Automate it at scale with Turbo Intruder or a script, and see if you can run it far past the intended limit.
  • Test the specific controls. Is the captcha token really validated server-side, or can it be reused or skipped on the API? Are the only limits per-IP, so IP rotation defeats them? Does a headless client get blocked?

In the Vulnerable Bank, I replayed the virtual-card creation request 100 times with Burp Intruder, and the server happily minted 100 distinct cards, with no per-account limit on a sensitive financial flow.

How to prove you are covered

  • Abuse-prone flows are explicitly identified and threat-modeled.
  • Anti-automation controls live on the API, not just the web UI: captcha, device fingerprinting, behavioral analysis.
  • Limits are keyed on more than IP (account, device, payment instrument), so rotation does not defeat them.
  • Business guardrails: purchase caps per user, reservation holds with expiry, cancellation limits, and sign-up throttling.
  • Monitoring of anomalous flow velocity.

Reference: API6:2023 Unrestricted Access to Sensitive Business Flows

API7:2023 - Server Side Request Forgery

SSRF occurs when an API fetches a URL you provide without validating where it points. You hand it an internal address, and it reaches inside its own network and brings the response back to you.

Is it vulnerable?

  • Does any endpoint fetch a remote resource from a client-supplied URL? Webhooks, "import from URL", avatar-by-URL, URL previews, and custom SSO.
  • Is that URL validated against an allowlist, or fetched blindly?
  • Are redirects followed? Is the raw response handed back to the client?

How I test for it

  • Find every URL-accepting input, then point it at an out-of-band listener like Burp Collaborator or interactsh to confirm the server makes the request. This also catches blind SSRF, where nothing comes back in the body, but a callback fires.
  • Aim inward. Try http://127.0.0.1:port and internal ranges to enumerate internal services by timing or error differences.
  • Go for cloud metadata, the highest-value target: for example, http://ip-address/latest/meta-data/ on AWS can leak IAM credentials.
  • Bypass weak filters with alternate IP encodings, a whitelisted URL that redirects inward, or http://expected.com@ip-address/
  • Try other schemes where accepted, like file:// for local file read.

I have hit both shapes of this. In vAPI's Server Surfer, I swapped a normal URL for file:///etc/passwd and decoded the Base64 the server returned. In the Vulnerable Bank Part 2, an external IP was blocked with a 403, but http://ip-address/internal/config.json sailed through the loopback and returned the database password and app secret key.

06-api7-ssrf

How to prove you are covered

  • The fetching component is network-isolated and cannot reach internal ranges, loopback, or the metadata IP.
  • Allowlists enforced for origins, URL schemes (http and https only), and ports. Accepted media types validated.
  • HTTP redirects are disabled, so a whitelisted host cannot redirect inward.
  • A hardened URL parser validates the final resolved IP, not just the string, with DNS-rebinding mitigation.
  • Raw upstream responses are not returned to clients. Cloud metadata hardened, for example, IMDSv2 on AWS.

Reference: API7:2023 Server Side Request Forgery

API8:2023 - Security Misconfiguration

This is the broad one. It spans the whole stack, from a missing TLS setting to a verbose error that leaks a stack trace to a CORS policy that trusts any origin. Injection lost its own spot in the 2023 list, and I tend to test for it here, since in practice it usually rides in on a misconfigured or unvalidated input path. That is my own placement, not an official OWASP mapping.

Is it vulnerable?

  • Is hardening missing anywhere in the stack, or are cloud permissions loose?
  • Are patches missing, or are unnecessary features and HTTP verbs enabled?
  • Is TLS missing, are security and cache-control headers absent, is CORS missing, or too permissive?
  • Do error messages include stack traces or internal details?

How I test for it

  • Check security headers: Strict-Transport-Security, Cache-Control: no-store on sensitive responses, X-Content-Type-Options. Missing cache-control on private JSON is a real finding.
  • Test CORS. Send Origin: https://evil.com and see if it is reflected in Access-Control-Allow-Origin alongside Access-Control-Allow-Credentials: true.
  • Tamper with methods. Send OPTIONS, PUT, DELETE, and TRACE requests, and look for verb-based bypass.
  • Force errors with malformed JSON and bad types, then read the stack traces and version banners that come back.
  • Probe for exposed files and defaults: /.git, /.env, /actuator, /swagger, and admin panels. nikto, nuclei, and dirsearch do this well.
  • Test for injection. When a login error leaks a SQL message, that is your cue. I used sqlmap to dump the whole database in VAmPI and to pull admin creds in vAPI, and a plain OR 1=1 -- bypassed login in Zero-Health.

In the Vulnerable Bank, sending Origin: http://meli.traleor.com got it reflected straight back in the CORS header, and the config file I pulled via SSRF was itself a misconfiguration.

How to prove you are covered

  • An automated, repeatable hardening baseline is applied to every environment and rechecked continuously.
  • Config reviewed across the whole stack: orchestration files, gateway, proxies, cloud permissions.
  • TLS is enforced everywhere with HSTS and modern ciphers.
  • An explicit per-endpoint HTTP verb allowlist. A proper CORS policy with an origin allowlist, never a wildcard, and credentials.
  • Enforced response schemas, including errors. Generic error messages, no stack traces to clients.
  • Patch management and dependency scanning in CI. Unnecessary features disabled.

Reference: API8:2023 Security Misconfiguration

API9:2023 - Improper Inventory Management

You cannot protect what you do not know you are running. This is the flaw of the forgotten staging host, the old v1 that never got the fix, the undocumented endpoint. The current production API might be locked down tight, while a beta host sitting next to it, wired to the same database, has none of the protections.

Is it vulnerable?

  • Is it unclear which environment a host runs in, who should reach it, and which version is live?
  • Is documentation missing or stale? Is there no retirement plan per version?
  • Is sensitive data shared with a third party with no inventory of the flow?

How I test for it

  • Enumerate versions. Fuzz /v1/, /v2/, /beta/, /internal/, and header-based versions. Then compare protections across them. An old version missing the current one's controls is the finding.
  • Discover docs and schemas: /swagger, /openapi.json, /api-docs, GraphQL introspection. These reveal endpoints you were not meant to see.
  • Hunt shadow and zombie endpoints. Mine historical URLs with waybackurls and pull endpoints out of JS bundles with LinkFinder.
  • Find non-production hosts. Enumerate dev., staging., beta., sandbox. subdomains with Subfinder, Amass, and Certificate Transparency logs.
  • Confirm retirement. Check that deprecated endpoints really return 410 or 404 and are not just hidden.

This is a pattern I keep exploiting:

In vAPI, /api9/v2/user/login returned a rate-limit header while the legacy /api9/v1/user/login had none, so I brute-forced the PIN on v1.

In the Vulnerable Bank, /api/v3/forgot-password masked the reset PIN while the legacy /api/v1/forgot-password returned it in plain JSON. The old version is almost always the weak one.

07-api9-inventory-map

How to prove you are covered

  • A maintained, automated inventory of every host with environment, version, exposure, and owner.
  • An inventory of every third-party integration with the data exchanged and its sensitivity.
  • Complete, auto-generated documentation kept in sync in CI, available only to authorized consumers.
  • A formal versioning and retirement policy, with retired versions truly decommissioned.
  • The same security controls are applied to all exposed versions and environments, not just the current production. No production data in non-prod.

Reference: API9:2023 Improper Inventory Management

API10:2023 - Unsafe Consumption of APIs

The newest item, and a shift in perspective. The other nine are about incoming requests to your API. This one is about the requests your API makes out to the third-party services it consumes. Developers trust data from a well-known API more than they trust user input, and that trust is the vulnerability.

Is it vulnerable?

  • Does it talk to other APIs over an unencrypted channel?
  • Does it validate and sanitize data from those APIs before using it or passing it downstream?
  • Does it blindly follow redirects? Does it enforce timeouts and response-size limits on third-party calls?

How I test for it

This one often requires some grey-box knowledge of the integrations, but there are also black-box approaches.

  • Map the third-party integrations first: enrichment services, webhooks, and upstream partners.
  • Check transport. Confirm the API calls upstreams over TLS with cert validation, not plain HTTP.
  • Inject through the upstream. Where you can influence what the third party returns, for example, by registering a business or repo whose fields you control, plant an SQLi or XSS payload, and see if the consuming API processes it unsanitized.
  • Test redirect handling. If you can make an upstream respond with a 308 to a host you control, does the API blindly re-send the request, secrets, and all? This overlaps with SSRF.
  • Push resource limits. Have the upstream return a huge or slow response and check for timeouts.

The closest demonstration I did is the Vulnerable Bank Part 2 AI chatbot. Its GET /api/ai/system-info reported "database_access": true, and by feeding it natural-language prompts, I got it to retrieve data that bypassed the app's own BOLA and RBAC checks. The backend consumed the AI's output with no guardrails, which is the shape of unsafe consumption.

How to prove you are covered

  • The vendor's security posture is assessed before integration.
  • All outbound calls are over TLS with certificate validation.
  • Data from third-party APIs is validated and sanitized before processing, storing, or forwarding. Treat it like user input.
  • No blindly followed redirects. An allowlist of permitted redirect destinations, with sensitive headers dropped on cross-host redirects.
  • Timeouts and response-size limits on every third-party interaction. Strict response-schema validation.

Reference: API10:2023 Unsafe Consumption of APIs

Reading a checklist and working through one are different things. I built an interactive companion to this article where you can tick each check off, filter by severity or status, expand any item to view the payloads and commands, watch your progress fill up, and export your findings when you are done. It is meant to sit open on a second screen while you test.

There is a printable version too, for when you want the checklist attached to an engagement report rather than open in a browser.

That's it!

If I could tattoo one lesson from all of this onto the back of my hand, it would be that eight of these ten come down to two questions: who are you, and are you allowed to do this? BOLA, BOPLA, BFLA, business flows, authentication, they are all authorization requests at different layers. Get authorization right, on every object, every property, every function, every version, and most of this list closes itself.

And remember the disclaimer at the top. Once these boxes are ticked, the real work of finding the flaw nobody else thought of begins.

See you in the next one!

Made With Traleor