# API Security Checklist: What a Real Penetration Test Actually Looks For
TL;DR: A real API pentest isn't a vulnerability scanner report — it's a manual walk through authorization boundaries (can user A touch user B's data?), authentication edge cases, rate limits, input validation, and mass assignment, organized around the OWASP API Security Top 10. This checklist mirrors that review, with the exact things a reviewer will try against each endpoint.
Most teams run a SAST tool, see a green check, and assume their API is fine. It isn't, because the vulnerability classes that actually get APIs breached — broken authorization, mass assignment, business logic abuse — aren't things a static scanner can find. They require someone to read the spec, understand what each endpoint is supposed to enforce, and then try to make it not enforce that.
This is the checklist a competent reviewer runs through, endpoint by endpoint, mapped to the 2023 OWASP API Security Top 10.
1. Broken Object Level Authorization (BOLA / IDOR) — API1:2023
OWASP ranked it #1 in the list's first edition in 2019, and it remains #1 in the 2023 update — objects reachable by ID with no ownership check.
What a reviewer tries on every endpoint that takes an ID:
- Swap the ID for another user's resource ID while authenticated as yourself. Does the API return their data?
- Try sequential or guessable IDs (
/orders/1001,/orders/1002) if the API uses integer or UUID-v1-style identifiers. - Test every HTTP verb on the object —
GETmight be checked whilePATCHorDELETEon the same route isn't. - Check nested resources:
/users/123/invoices/456— does the API verify invoice 456 actually belongs to user 123, or just that invoice 456 exists?
The fix is enforcing object ownership at the data-access layer, not just the route layer, so it can't be forgotten on a new endpoint:
// Bad: trusts the ID from the URL without checking ownership
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
res.json(invoice);
});
// Good: ownership check is part of the query, not a separate step
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findFirst({
where: { id: req.params.id, userId: req.user.id },
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});2. Broken Authentication — API2:2023
Checklist items:
- Do password reset and email-change tokens expire, and are they single-use?
- Can the JWT
algheader be flipped tonone, or an RS256 token re-signed as HS256 using the public key as the HMAC secret? (A well-known JWT library confusion attack.) - Are refresh tokens revocable server-side, or is the only invalidation mechanism token expiry?
- Is there a lockout or backoff on login and OTP endpoints, or can they be brute-forced?
- Are API keys transmitted only over TLS and never logged in access logs or error traces?
3. Broken Object Property Level Authorization / Mass Assignment — API3:2023
This category merged the old "Excessive Data Exposure" and "Mass Assignment" risks in the 2023 revision. Mass assignment happens when an endpoint blindly binds the request body to a model, so a client can set fields it was never meant to touch. The canonical example is from 2012, when developer Egor Homakov used a mass-assignment flaw in GitHub's Rails app to attach his SSH public key to the Rails project's account, then pushed an innocuous file to the Rails repository to prove it (The Register).
What a reviewer tries:
- Add extra, unexpected fields to a
PATCH/POSTbody —role,isAdmin,accountBalance,verified— and see if any get accepted. - Compare the response payload against what the frontend actually renders. If the API returns internal fields (
passwordHash,internalNotes,costPrice) that the UI just hides, that's excessive data exposure even without a mass-assignment bug.
The fix is an explicit allowlist of writable fields, never a denylist and never raw model binding:
// Bad: whatever the client sends gets written
await db.user.update({ where: { id: req.user.id }, data: req.body });
// Good: only known-safe fields are ever passed through
const { displayName, bio } = req.body;
await db.user.update({
where: { id: req.user.id },
data: { displayName, bio },
});The OWASP Mass Assignment Cheat Sheet has framework-specific guidance (DTOs in TypeScript/Java, strong_params in Rails, serializer allowlists in Django REST Framework).
4. Unrestricted Resource Consumption — API4:2023
This replaced the old "Lack of Resources & Rate Limiting" category and broadened it to cover compute, memory, storage, and cost — not just request volume.
Checklist:
- Is there a per-user or per-API-key rate limit, and does it apply to authenticated and unauthenticated traffic?
- Can pagination parameters be abused —
?pageSize=999999— to force an unbounded query? - Are file upload endpoints capped on size and count, and is decompression bounded (zip bombs)?
- Do expensive endpoints (search, export, PDF generation, bulk operations) have their own tighter limits than simple
GETrequests?
A minimal example using express-rate-limit (v8), scoped per API key rather than globally:
const { rateLimit, ipKeyGenerator } = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 100,
// Key on the *validated* API key (set by your auth middleware), not the raw
// header — otherwise an attacker can rotate made-up keys to dodge the limit.
// Fall back to the client IP; v8 expects ipKeyGenerator here so IPv6
// users can't bypass the limit by cycling addresses within their subnet.
keyGenerator: (req) => req.apiKey?.id ?? ipKeyGenerator(req.ip),
standardHeaders: 'draft-8',
legacyHeaders: false,
});
app.use('/api/', apiLimiter);(In express-rate-limit v8, a custom keyGenerator that returns req.ip without ipKeyGenerator triggers an ERR_ERL_KEY_GEN_IPV6 validation error, and limit replaces the deprecated max option.)
5. Broken Function Level Authorization — API5:2023
Different from BOLA: this is about whether a user can call an endpoint at all, not whether they can reach a specific object. Classic pattern — admin-only routes that exist and work, but the server never actually checks the caller's role.
Checklist:
- Take a low-privilege user's token and call every admin route directly (not through the UI, which may just hide the button).
- Check for inconsistent authorization between REST and any GraphQL or internal RPC surface serving the same data.
- Verify that role checks happen server-side per request, not once at login and cached client-side.
6. Unrestricted Access to Sensitive Business Flows — API6:2023
New in the 2023 edition. This isn't a technical bug so much as an abuse-of-scale problem: an endpoint works exactly as designed, but nothing stops it from being hit thousands of times per minute — ticket-buying bots, bulk account creation, coupon-code farming, automated scalping.
Checklist: does the flow need CAPTCHA, device fingerprinting, purchase-velocity limits, or human review at scale, above and beyond generic rate limiting?
7. Server-Side Request Forgery (SSRF) — API7:2023
Any endpoint that accepts a URL from the client and fetches it server-side (webhooks, image-by-URL upload, "import from link" features) is a candidate.
Checklist:
- Can the URL be pointed at
169.254.169.254(cloud metadata endpoints) orlocalhost/internal IP ranges? - Is there an allowlist of permitted destination hosts/schemes, and is redirect-following restricted so an allowlisted URL can't 302 to an internal one?
8–10. Security Misconfiguration, Improper Inventory Management, Unsafe Consumption of APIs — API8/9/10:2023
- Misconfiguration: verbose stack traces in error responses, permissive CORS (
Access-Control-Allow-Origin: *on authenticated routes), default credentials left on admin tooling, missing security headers. - Inventory management: shadow and zombie endpoints — old API versions (
/v1/...) still deployed and unpatched while the docs only mention/v2/. A reviewer will fingerprint every accessible version, not just the documented one. - Unsafe consumption: if your API calls a third-party API, does it validate that response the same way it would validate client input? Trusting an upstream partner's response blindly can reintroduce every vulnerability above through the back door.
The Checklist, Condensed
| Category | OWASP ID | Core question a reviewer asks |
|---|---|---|
| Broken Object Level Authorization | API1:2023 | Can I access another user's object by changing an ID? |
| Broken Authentication | API2:2023 | Can tokens be forged, replayed, or brute-forced? |
| Broken Object Property Authorization / Mass Assignment | API3:2023 | Can I write fields I shouldn't, or read fields the UI hides? |
| Unrestricted Resource Consumption | API4:2023 | Can I exhaust rate, memory, storage, or cost limits? |
| Broken Function Level Authorization | API5:2023 | Can a low-privilege user call a high-privilege endpoint? |
| Unrestricted Sensitive Business Flows | API6:2023 | Can this flow be automated/abused at scale? |
| SSRF | API7:2023 | Can a server-fetched URL reach internal infrastructure? |
| Security Misconfiguration | API8:2023 | Are defaults, headers, and error output hardened? |
| Improper Inventory Management | API9:2023 | Do old/undocumented API versions still work? |
| Unsafe Consumption of APIs | API10:2023 | Is upstream data treated as untrusted input? |
A team running a real penetration test — whether in-house or through a firm like Wise Hustlers' cybersecurity practice — should expect a report structured around exactly this list, with reproduction steps per finding, not just a tool-generated CVE dump.
FAQ
What's the difference between BOLA and IDOR?
IDOR (Insecure Direct Object Reference) is the older, broader OWASP Web Top 10 term. BOLA (Broken Object Level Authorization) is the API-specific framing OWASP adopted for the API Security Top 10 — same underlying flaw (missing ownership check on an ID-referenced object), different list.
Does automated scanning cover the OWASP API Security Top 10?
Partially. Tools catch misconfiguration, missing headers, and some injection patterns reliably. They generally cannot catch BOLA, broken function-level authorization, or business-flow abuse, because those require understanding what the correct authorization behavior should be for a given user and object — that's a judgment call a scanner can't make.
How often should an API get a real penetration test?
Our recommendation is at least annually (or whatever cadence your compliance framework or customer contracts require, if stricter), plus after any major authorization or authentication change, and before launching a new API version. Continuous automated scanning fills the gaps between manual tests but doesn't replace them.
Is mass assignment still relevant if I use GraphQL instead of REST?
Yes. GraphQL mutations that map input types directly onto database models have the identical failure mode — an unchecked input object can set fields the client shouldn't control. The fix (explicit allowlists, no raw model binding) is the same regardless of transport.