OWASP API Top 10: tests you can run, and a checklist
The OWASP API Security Top 10 (2023 edition) lists ten API risk categories, and each one maps to at least one concrete test. We turned four of them (API1, API3, API4 and API5) into a small local API with 12 scripted requests. The vulnerable mode failed 5 of the 12 checks, and the fixed mode passed all 12, including every positive control.
This is a scripted Python and curl experiment with downloadable source and responses. No AI agent or model ran the test.
What the OWASP API Security Top 10 is
The OWASP API Security Top 10 is a ranked list of the most common API risk categories. This post uses the 2023 edition. We checked each risk ID and name below against the OWASP pages on 10 November 2026. The list names categories and does not give a pass or fail standard. An "API security checklist" is what a team builds from it: a set of concrete requests with expected answers.
For each risk, this post gives one plain sentence, one test and, where the lab has one, a link to a post that reproduces the flaw.
The ten risks, with one test each
API1:2023 Broken Object Level Authorization
An endpoint takes an object ID from the client and returns or changes that object without checking that the caller may.
Test: Log in as two users. Request user A's object ID with user B's token. Expect 403 or 404 and none of A's fields in the body.
Reproduced in api-authorization-testing, idor-vulnerability and tenant-isolation.
API2:2023 Broken Authentication
Login, token or session handling lets someone act as another user.
Test: On staging, send 20 wrong passwords for one account from one client and check for throttling or lockout. Replay an expired token and a token with one changed character. Expect 401 each time.
Related fixtures: 2fa-bypass and session-fixation.
API3:2023 Broken Object Property Level Authorization
An endpoint exposes properties the caller should not see, or accepts writes to properties the caller should not change.
Test: Send a PATCH to your own profile with {"role":"admin"}, then read the profile. Compare every returned field with the documented schema.
Reproduced in mass-assignment, and in the fixture below (cases 5 and 6).
API4:2023 Unrestricted Resource Consumption
Nothing caps the size, count or frequency of requests, so one client can use the server's CPU, memory, bandwidth or paid upstream calls.
Test: Ask a list endpoint for limit=100000, post a 10 MB body, and send a burst of 100 requests. Expect 400, 413 or 429.
Rate limiting is in api-rate-limiting. The page-size cap is in the fixture below (case 11).
API5:2023 Broken Function Level Authorization
A privileged function is reachable by an ordinary user because the route has no role check.
Test: Take every admin route from your API description and call it with an ordinary user's token, using each HTTP method. Expect 403.
Related: broken-access-control. The fixture below covers it in case 7.
API6:2023 Unrestricted Access to Sensitive Business Flows
A flow such as sign-up, checkout or ticket booking works as designed and still harms the business when a script runs it at scale.
Test: List the flows that have business value. On staging, script one of them at ten times human speed from a single account and see whether anything slows or blocks it.
The lab has no published fixture for this risk yet.
API7:2023 Server Side Request Forgery
The server fetches a URL that the client supplied without checking where it points, so a caller can reach internal hosts through it.
Test: Give every URL-taking parameter the address of a listener you control, then check whether the server connects. Repeat with a loopback address.
Reproduced in ssrf.
API8:2023 Security Misconfiguration
Unsafe defaults, verbose errors, open CORS rules, missing hardening or unneeded features are left on.
Test: Send a malformed request and read the error body for stack traces. Read the response headers on a normal and an error response. Probe common debug paths on staging.
The closest fixture is graphql-introspection, which covers one such misconfiguration. Most of this risk has no published fixture yet.
API9:2023 Improper Inventory Management
Old API versions, forgotten hosts or undocumented endpoints stay reachable and unpatched.
Test: Compare the documented endpoint list with a month of access-log paths and your DNS records. Request old version prefixes such as /v1/ and expect a deliberate answer.
The lab has no published fixture for this risk yet.
API10:2023 Unsafe Consumption of APIs
Your API trusts data from a third-party API more than it trusts client input.
Test: Point a staging copy at a stub upstream that returns oversized, malformed and redirecting responses. Check that your code validates them, applies timeouts and refuses unexpected redirects.
The lab has no published fixture for this risk yet.
Scope of the checklist fixture
The fixture is a toy. It has four users (alice, bob, carol and an admin) with public demo tokens, two notes, a profile endpoint, an admin user list and a search endpoint over 500 synthetic rows. It always binds 127.0.0.1 and the runner picks free ports. It demonstrates API1, API3, API4 and API5 only. The other six risks need infrastructure, third parties or business context that a server this small cannot show.
The fixture is intentionally insecure in vulnerable mode. Run it only on your own machine. For a real system, get written permission first and agree hosts, accounts and stopping conditions with its owner. The Acceptable Use Policy describes authorized use, and this post gives no permission to test another system.
Reproduce the API security checklist run
Download fixture.py and verify.py into one directory. You need Python 3.9 or later and curl. Read the source, then run:
python3 verify.py --output rerun
The runner starts the vulnerable server, sends 12 curl requests, stops it, then repeats against the fixed server. It refuses an existing output directory. Each case states the secure expectation (a status and a body check), so a vulnerable server shows FAIL where it has the flaw. Exit code 0 means both modes behaved as designed. It does not mean the vulnerable mode is safe. Our recorded run used ports 53529 (vulnerable) and 53542 (fixed), Python 3.15.2 and curl 8.7.1 on macOS.
Observed results
Each status below links to the raw response. Controls are the requests where a correct server and a flawed one should give the same answer.
| # | Request | Risk | Secure status | Vulnerable | Fixed |
|---|---|---|---|---|---|
| 1 | alice-reads-own-note | API1 control | 200 | 200 PASS | 200 PASS |
| 2 | bob-reads-alice-note | API1 | 403 | 200 FAIL | 403 PASS |
| 3 | anonymous-reads-note | API1 control | 401 | 401 PASS | 401 PASS |
| 4 | bob-edits-display-name | API3 control | 200 | 200 PASS | 200 PASS |
| 5 | bob-sets-own-role | API3 | 400 | 200 FAIL | 400 PASS |
| 6 | bob-reads-own-profile | API3 | 200 | 200 FAIL | 200 PASS |
| 7 | carol-lists-admin-users | API5 | 403 | 200 FAIL | 403 PASS |
| 8 | admin-lists-admin-users | API5 control | 200 | 200 PASS | 200 PASS |
| 9 | anonymous-lists-admin-users | API5 control | 401 | 401 PASS | 401 PASS |
| 10 | alice-search-limit-50 | API4 control | 200 | 200 PASS | 200 PASS |
| 11 | alice-search-limit-100000 | API4 | 400 | 200 FAIL | 400 PASS |
| 12 | alice-search-bad-limit | API4 control | 400 | 400 PASS | 400 PASS |
Five checks failed in vulnerable mode and none failed in fixed mode. Bob read Alice's note (case 2). Bob changed his own role to admin with a 200 (case 5), and his next profile read returned "role": "admin" (case 6). Carol, an ordinary user, received the user list (case 7). A request for limit=100000 returned all 500 rows, about 17 KB (case 11). The 7 control requests passed in both modes, so the fixes did not break legitimate use.
Verify a finding before you report it
A status code alone is weak evidence. Case 2 counts as a finding because three facts hold together: the token belongs to Bob, the note belongs to Alice, and the body contains Alice's note text. Case 6 is the confirmation for case 5, because the 200 on the write could have been an ignored field. Only the follow-up read shows the role changed. For API4, record the response size and time against the documented limit. A large response alone is a finding only if the product policy says it should be capped.
A useful report names the method, path, identity, expected denial, observed status and disclosed data. This fixture shows disclosure of synthetic data. It does not establish a severity rating for any real system.
The fixes and the retest
The same source has both modes. These are the four checks that fixed mode enables, copied from fixture.py with surrounding code removed:
# API1: object ownership
if fixed and note["owner"] != who:
return self.reply(403, {"error": "forbidden"})
# API3: allowlist of writable fields
if self.server.mode == "fixed":
for field in changes:
if field not in EDITABLE_FIELDS:
return self.reply(400, {"error": "unknown_field", "field": field})
# API4: hard cap on page size
if fixed and limit > MAX_LIMIT:
return self.reply(400, {"error": "limit_too_large", "max": MAX_LIMIT})
# API5: role check on the route
if fixed and profiles[who]["role"] != "admin":
return self.reply(403, {"error": "forbidden"})
The retest reran the same 12 requests. Bob's read of Alice's note returned 403, the role write returned 400 with {"error": "unknown_field", "field": "role"}, Carol's admin request returned 403 and the oversized search returned 400. Alice and Bob still read their own data, the admin still listed users, and the display-name edit still returned 200. These checks cover this toy policy. Real systems add tenants, sharing, per-action roles and rate limits keyed to identity.
Evidence bundle
- Machine-readable results: all 24 cases, expectations, observed statuses, response hashes, versions and source SHA-256.
- Pass/fail table as written by the runner, and recorded curl commands. The ports belong to the finished run.
- SHA-256 manifest, README, fixture.py and verify.py. The manifest is an integrity check, not independent attestation.
To adapt the checklist, edit the CASES list in verify.py: each entry is a token, method, path, optional JSON body, the secure status and a body check. Point the base URL at a staging system you are authorized to test and keep the controls.
What this did not test
We tested four of ten risks against one toy server with 12 requests. The fixture has no real authentication flow, no sessions, no TLS, no upstream calls, no database and no concurrency. We did not test API2, API6, API7, API8, API9 or API10 in this fixture. Rate limiting is not tested here. The size cap in case 11 addresses only one part of API4. Passing this checklist says nothing about an unrelated API.
Copyable API security checklist
- API1: user B's token cannot read or change user A's objects (403 or 404, no fields leaked).
- API2: wrong-password bursts are throttled; expired and altered tokens return 401.
- API3: a PATCH with
role,owneroris_adminis rejected; responses contain only documented fields. - API4: page size, body size and request rate all have limits that return 400, 413 or 429.
- API5: every admin route returns 403 for an ordinary token, on every HTTP method.
- API6: the top business flows slow or block scripted use at ten times human speed.
- API7: URL parameters do not make the server call a listener you control or a loopback address.
- API8: error bodies carry no stack traces, security headers are set and debug paths are closed.
- API9: the documented endpoint list matches logs and DNS, and old versions are retired on purpose.
- API10: a stub upstream returning bad data and redirects does not break or poison your API.
- Every denied case has a matching allowed control that still returns 200.
Excession Labs runs this kind of API testing in its security assessments. See how findings like these look in a sample report.