18 August 2026

Pen-Testing the OWASP API Security Top 10

Rather than just talking about security with developers, I like to put together hands-on sessions to really drive home the risks. A common area I like to cover is API security testing and threat modelling. APIs are the connective tissue of modern applications — and, as I wrote about more recently, that same connectivity is exactly what makes them such an attractive target. A web front-end might be hardened and pen-tested to death, while the API quietly sitting behind it leaks data, trusts the client far too much, or enforces authorisation in name only.

The OWASP API Security Top 10 captures the flaws that frequently show up. The most recent (2023) list looks like this:

  • API1 — Broken Object Level Authorization (BOLA)
  • API2 — Broken Authentication
  • API3 — Broken Object Property Level Authorization (the old Excessive Data Exposure and Mass Assignment merged into one)
  • API4 — Unrestricted Resource Consumption
  • API5 — Broken Function Level Authorization
  • API6 — Unrestricted Access to Sensitive Business Flows
  • API7 — Server Side Request Forgery
  • API8 — Security Misconfiguration
  • API9 — Improper Inventory Management
  • API10 — Unsafe Consumption of APIs

Reading a list is one thing; getting hands-on is another. The best way to internalise these categories is to attack a deliberately vulnerable target. For that I like VAmPI — the Vulnerable API — a small Flask REST API that ships with most of these flaws baked in and, helpfully, a switch to turn its vulnerabilities on and off so you can compare vulnerable and remediated behaviour side by side.

Setting up the target

The quickest way to get a copy running is with Docker:

docker run -d -p 5000:5000 erev0s/vampi:latest

VAmPI exposes a tiny domain: users can register and log in, receive a JWT, and post books. Each book has a title and a secret that only its owner should be able to read. That is enough surface area to demonstrate the majority of the Top 10.

Before we start poking at it, we seed the database with some dummy data:

curl http://127.0.0.1:5000/createdb

This gives us a handful of pre-populated users (name1, name2, admin) and their books. From here on I’ll treat the API as a black box, the way you would at the start of an engagement.

Reconnaissance

The first job is always to understand the application. What endpoints exist, what do they expect, and what do they hand back? VAmPI publishes an OpenAPI/Swagger definition, but even without one we can enumerate the obvious resources:

curl http://127.0.0.1:5000/users/v1

curl http://127.0.0.1:5000/books/v1

Listing the users returns their usernames and email addresses without any authentication at all. That is worth noting immediately — an unauthenticated endpoint that enumerates accounts is both a data exposure problem and a gift for anyone building a target list for credential stuffing.

To do anything as a user, we need an identity. Registration and login are unauthenticated by design:

curl http://127.0.0.1:5000/users/v1/register \
  -d '{"email":"attacker@example.com","username":"foo","password":"bar"}' \
  -H 'Content-Type: application/json'

curl -X POST http://127.0.0.1:5000/users/v1/login \
  -d '{"username":"foo","password":"bar"}' \
  -H 'Content-Type: application/json' | jq

The login response contains a JWT. We’ll pass that as a bearer token on subsequent requests. Throughout the rest of this post, replace [token] with the JWT you were issued.

API3 — Broken Object Property Level Authorization (Excessive Data Exposure)

The listing endpoint we saw earlier returns a trimmed view of each user. VAmPI also ships a debug endpoint that returns everything:

curl http://127.0.0.1:5000/users/v1/_debug

The response includes every user’s password in cleartext, along with their admin flag. This is excessive data exposure: the API serialises the entire object and relies on the client to only display the fields it needs. A debug route left reachable in production is also a textbook Security Misconfiguration. An endpoint can return the full database record when the UI only ever renders a name and an avatar, and the sensitive fields are one curl away.

API1 — Broken Object Level Authorization (BOLA / IDOR)

BOLA — historically known as an insecure direct object reference — is consistently the number one API risk, and for good reason. The idea is simple: the object identifier is in the request, the server looks the object up, but it never checks that this caller is allowed to see that object.

Each book in VAmPI has a secret only its owner should read. Once we’re authenticated, we can list the books and then ask for one that belongs to another user:

curl http://127.0.0.1:5000/books/v1

curl http://127.0.0.1:5000/books/v1/bookTitle1 \
  -H 'Authorization: Bearer [token]'

Even though bookTitle1 belongs to name1 and not to us, the secret comes back. The endpoint authenticates the caller (we hold a valid token) but never authorises the object — it never asks whether the holder of this token owns this book. Any time you see a resource addressed by a predictable identifier, this is the first thing to test: authenticate as user A, then request user B’s objects.

API2 — Broken Authentication

VAmPI’s tokens are JWTs, and in its vulnerable configuration they are signed with a weak, hardcoded secret. That matters because a JWT’s integrity rests entirely on the signing key. If you can recover the key, you can forge a token for anyone — including admin.

We can attack the signature offline. Capture a token and feed it to a cracker such as jwt_tool or hashcat’s JWT mode (-m 16500) with a wordlist:

hashcat -a 0 -m 16500 token.jwt /usr/share/wordlists/rockyou.txt

Once the secret falls, forging a token with "admin": true or a different sub is trivial, and the server will accept it as genuine. Weak or hardcoded signing keys, missing expiry, and accepting the none algorithm are the usual suspects here — always inspect the token’s header and claims before assuming the authentication is sound. It’s also worth checking the registration and login flows for the other half of broken authentication: no rate limiting, no lockout, and verbose errors that tell you which half of the credential pair was wrong.

API5 — Broken Function Level Authorization

Where BOLA is about which objects you may touch, BFLA is about which actions you may perform. Administrative functions must verify that the caller actually holds the privileged role, not merely a valid session.

VAmPI lets us change another user’s password, and delete users, without checking that we’re entitled to:

curl -X PUT http://127.0.0.1:5000/users/v1/name1/password \
  -H 'Authorization: Bearer [token]' \
  -H 'Content-Type: application/json' \
  -d '{"password":"pwned"}'

curl -X DELETE http://127.0.0.1:5000/users/v1/name1 \
  -H 'Authorization: Bearer [token]'

An ordinary, non-admin token is enough to reset another account’s password — an instant account takeover — or to delete users outright. The function exists and is reachable; the only thing missing is the check that the caller is allowed to call it. When testing, take every privileged action you can find and try it with a low-privilege token.

API3 (again) — Mass Assignment

The other half of Broken Object Property Level Authorization is mass assignment: the server binds request fields straight onto an object, including fields the client was never meant to control. The registration endpoint accepts email, username and password — but what happens if we also send the internal admin flag?

curl http://127.0.0.1:5000/users/v1/register \
  -d '{"email":"evil@example.com","username":"evil-admin","password":"bar","admin":true}' \
  -H 'Content-Type: application/json'

The undocumented admin property is happily bound onto the new user, and we’ve minted ourselves an administrator at registration time. The lesson is to probe for object properties beyond those the documentation advertises — internal flags, ownership fields, prices, statuses — and see which ones the server will let you set.

SQL Injection

SQL injection no longer has its own slot in the API Top 10, but it lives on under Security Misconfiguration and it’s still very much worth testing. VAmPI builds at least one query by string concatenation, so a single quote in a path parameter is enough to break out:

curl "http://127.0.0.1:5000/users/v1/foo'"

A malformed response or SQL error is the tell that user input is reaching the query engine unsanitised. From there the usual escalation applies — boolean and UNION-based extraction to pull data the endpoint was never meant to return. As always, the fix is parameterised queries, never string building.

API4 — Unrestricted Resource Consumption

Finally, notice what isn’t there: any throttling. The login endpoint will accept as many attempts as you can send it:

curl -X POST http://127.0.0.1:5000/users/v1/login \
  -d '{"username":"admin","password":"guess"}' \
  -H 'Content-Type: application/json'

Combine that with the unauthenticated user listing from earlier and you have everything needed for an unthrottled credential-stuffing or password-spraying attack. In a proxy such as Burp or ZAP you can set the username or password as a payload position and iterate freely. The lack of rate limiting also opens the door to more basic denial-of-service — VAmPI even includes a regex that can be pushed into catastrophic backtracking (ReDoS) with a crafted input. Any endpoint that does real work — authentication, search, file processing — needs limits on how often and how expensively it can be called.

Automating the repetitive parts

Manual testing is where the interesting findings come from, but it doesn’t scale, and you don’t want to rediscover the same missing security header on every endpoint by hand. This is where dynamic application security testing (DAST) earns its place. Tools built for APIs — OWASP ZAP, or a purpose-built scanner like StackHawk — can ingest the OpenAPI definition, authenticate, and hammer every documented endpoint with a battery of active checks at a speed no human can match.

The important caveat, and one I stress with developers, is that DAST is a complement to manual testing, not a replacement for it. A scanner is very good at breadth — misconfigurations, injection reflections, missing controls across hundreds of endpoints — and poor at the business-logic flaws that dominate the API Top 10. No automated tool is going to understand that book number one belongs to someone else; BOLA, BFLA and broken authentication almost always need a human who understands the application’s intent. Wire the scanner into CI so it runs on every build, and keep the manual pen-test for the logic.

Remediating and re-testing

VAmPI’s kill switch is genuinely useful here. Flip it into non-vulnerable mode and walk back through every request above. The _debug route disappears, object-level and function-level authorisation checks reject requests for objects and actions that aren’t yours, the mass-assignment admin flag is ignored, queries are parameterised, and the JWT handling tightens up. Re-running your attacks against the fixed build is how you validate a mitigation rather than assuming it works — the same discipline applies to closing findings from a real threat model.

Wrapping up

If there’s a single thread running through the API Top 10, it’s misplaced trust in the client. The server authenticates who you are but forgets to check what you’re allowed to see (BOLA) or do (BFLA); it takes the fields you send at face value (mass assignment); it hands back more than it should (excessive data exposure); and it assumes you’ll play fair on volume (no rate limiting). A vulnerable target like VAmPI makes those abstractions concrete, and half an hour of curl against it is worth more than any number of slides.

If you’re a developer, the takeaway is to threat model early and assume every request is hostile. If you’re testing, work through the Top 10 methodically, authenticate as more than one user, and never trust that a control exists just because the usual path behaves.

References

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.