Cracking DVAPI — A Beginner's Guide to the OWASP API Top 10

A full walkthrough of DVAPI, an intentionally vulnerable API covering all 10 entries from the OWASP API Security Top 10. From BOLA to NoSQL injection, one at a time.

I actually finished this back in 2024. Had the whole thing documented, notes everywhere, screenshots taken. Then I just... never posted it. It sat in my drafts collecting dust for over a year while I kept telling myself I'd clean it up "this weekend." Today I finally got tired of looking at it and decided to just put it out.

Anyway — DVAPI (Damn Vulnerable API) is a deliberately insecure API built to cover every entry in the OWASP API Security Top 10. Think of it like DVWA but for APIs. It runs locally at 127.0.0.1:3000, comes with a Swagger UI, and is genuinely one of the better hands-on resources for getting comfortable with API-specific bugs. I went through the whole thing as part of a security assessment and this is the writeup I should've posted a long time ago.

For bugs that had multiple scenarios I'm keeping it to one — if you get the technique once, the rest are just the same thing on a different endpoint.

1. Broken Object Level Authorization (BOLA)

GET /api/getNote?username={username}

BOLA sits at the top of the OWASP API list for a reason — it's everywhere and it's embarrassingly easy to miss during development. The idea is simple: the API uses some identifier (in this case a username) to decide what data to return, but never actually checks whether the person asking owns that data. Your token just needs to be valid. Whose data you ask for? Totally up to you.

Step 1 — Normal request

Log in as a regular user and send a request to /api/getNote?username=youruser through Burp. Works fine, returns your notes.

Request to /api/getNote with own username returning notes
Fetching your own notes — works as expected.

Step 2 — Change the username to Admin

Now just swap the username parameter to Admin while keeping your own auth token. The server never checks whether your token belongs to that user — it just runs the query and hands the data back. Admin's notes, including the flag, come right out.

BOLA exploit — accessing Admin notes with a different user's token
Changed username to Admin, kept own token — Admin's notes come back without any complaint.
flag{bola_15_ev3rywh3r3}

The fix is just tying the authorization check to the token, not the parameter. If your token says you're user A, you should only ever get user A's data — regardless of what username you put in the request.

2. Broken Authentication — Weak JWT Secret

GET /api/user/{username}

A JWT is only as trustworthy as the secret used to sign it. If that secret is something like password1 or — as we're about to see — secret123, then anyone who figures it out can mint their own tokens and claim to be whoever they want. Including admin.

Step 1 — Find the secret

Capture any authenticated request to /api/user/{username} in Burp Suite Pro and throw it at the active scanner. It flags the JWT almost immediately — weak HMAC secret, value: secret123.

Burp Suite dashboard showing JWT HMAC secret key is 'secret123'
Burp's scanner catches the weak JWT secret straight away.

Step 2 — Forge an admin token

Head to jwt.io, paste in the captured token, flip isAdmin to true in the payload, and enter secret123 as the signing secret. Copy the generated token.

jwt.io interface showing isAdmin set to true and secret123 as signing key
Forged token at jwt.io — isAdmin true, signed with the cracked secret.

Step 3 — Use the forged token

Swap the Authorization header in Burp for the forged token and resend. The server checks the signature — valid, because we used the right secret — and just hands back the admin data. Flag included.

Response from /api/user endpoint using forged admin token showing the flag
Forged token accepted — admin endpoint returns the goods.

3. Security Misconfiguration — Stack Trace Disclosure

POST /api/profile

This one's less of an active exploit and more of a "the developers forgot to turn off debug mode" situation. Send a request with a slightly broken JWT — just change one character in the signature — and instead of getting a clean 401 Unauthorized, the server panics and dumps its entire stack trace into the response. Framework version, internal file paths, function names — all of it just sitting there in plain text.

On its own this doesn't give you access to anything. But as recon it's gold — you know exactly what's running under the hood, which makes finding the next bug a lot easier.

Stack trace error response from /api/profile after sending malformed JWT
One bad character in the JWT and the whole stack trace spills out.

This was reproducible across pretty much every authenticated endpoint in the app — not just /api/profile. The fix is turning debug mode off in production and returning a generic error message instead of the actual exception.

4. Broken Object Property Level Authorization

POST /api/register

Also known as mass assignment — where an API blindly applies everything you send in the request body to the object it's creating, without checking whether you're actually allowed to set those fields. The classic example is setting your own role to admin during signup. Here it's a little more game-like: you can set your own score.

Step 1 — Sneak in an extra field

Capture the POST /api/register request. Normal body is just username and password. Add "score": 10000 and send it.

POST /api/register HTTP/1.1
Content-Type: application/json

{
  "username": "testuser",
  "password": "pass123",
  "score": 10000
}
Burp request to /api/register with injected score property set to 10000
Extra score field added to the registration body — server takes it without question.

Step 2 — Check if it actually worked

Log in with the new account and hit /api/score with its token. Score: 10000. The API wrote the injected value straight into the user object without validating whether that field should even be settable on registration.

Response from /api/score showing score of 10000 from the injected property
Score is 10000 — the injection worked perfectly.

5. Unrestricted Resource Consumption

POST /api/profile/upload

The profile picture upload endpoint has zero size restrictions. You can throw a file of basically any size at it and the server will try to process it — happily eating up disk space and memory while it does. Not flashy, but in a real environment this kind of thing can take down a server or rack up cloud storage bills fast.

Step 1 — Create a large dummy file

Quick one-liner with dd to generate a 100MB file:

# Create a 100MB dummy image
dd if=/dev/urandom bs=1M count=100 > bigfile.jpg
Terminal showing dd command creating a 100MB file
100MB file generated in the terminal — ready to upload.

Step 2 — Upload it

Open the DVAPI Swagger UI, go to POST /api/profile/upload, select the file, and execute. Capture it in Burp to confirm what went through. No rejection, no size error — just a clean 200.

Burp showing the large file upload request being sent to /api/profile/upload
100MB upload accepted — no limit enforced anywhere.

6. Broken Function Level Authorization

DELETE /api/user/{username}

BFLA is similar to BOLA but at the function level — instead of accessing someone else's data, you're invoking functionality you're not supposed to have access to. Here, the DELETE method on the user endpoint is admin-only. Except the server never actually checks that when you call it.

Step 1 — Start with a normal GET

Send a GET /api/user/targetuser with a regular user token. Works fine, returns the profile.

Normal GET request to /api/user endpoint returning user profile
Normal GET — profile data comes back fine.

Step 2 — Switch to DELETE

In Burp Repeater, right-click and change the method to DELETE. Send it with the same regular user token. User gets deleted. No admin check, no confirmation, nothing.

DELETE request to /api/user succeeding with a regular user token
Same token, different method — account deleted without any privilege check.

7. Unrestricted Access to Sensitive Business Flows

POST /api/addTicket

This is the kind of vulnerability that doesn't look dangerous until you think about it for a second. The ticket submission endpoint works fine for a normal user submitting one request. But there's nothing stopping you from sending that same request a thousand times — no rate limiting, no CAPTCHA, no per-user quota. A spam attack or automated abuse of this flow would go completely unchecked.

Step 1 — Capture the ticket request

Log in, open the Ticket section in the UI, submit one ticket, intercept it in Burp.

Captured ticket creation request in Burp from the browser UI
Ticket request captured in Burp — now we automate it.

Step 2 — Spam it with Intruder

Send to Burp Intruder, set payload type to Null payloads, generate 99 iterations, run the attack. Every single request returns 200. No throttling anywhere.

Burp Intruder results showing 99 successful ticket creations with 200 status
99 tickets submitted — all 200 OK. Rate limiting simply doesn't exist here.

8. Server-Side Request Forgery (SSRF)

POST /api/addNoteWithLink

SSRF is one of my favourites — the idea of using the server as your own personal proxy to reach things you're not supposed to touch is just satisfying. The /api/addNoteWithLink endpoint takes a URL, fetches it server-side, and saves the content as a note. No URL validation. So naturally, we point it at an internal service the outside world can't reach.

There's an internal service on 127.0.0.1:8443 that serves a flag. Normally inaccessible from outside. But we're not fetching it — the server is.

POST /api/addNoteWithLink HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token>

{
  "url": "http://127.0.0.1:8443/flag"
}
SSRF exploit — Burp request changing URL to internal service at 127.0.0.1:8443/flag
Pointed the URL at the internal service — server fetches it and hands the flag back to us.

9. Improper Inventory Management

GET /api/allChallenges

This one's almost too simple. There's a /api/allChallenges endpoint that returns challenge data filtered by a type parameter. The intended value is released. But there's also unreleased content sitting in the database — and nothing stops you from just asking for it by name.

GET /api/allChallenges?type=unreleased HTTP/1.1
Authorization: Bearer <token>
Response from /api/allChallenges with type=unreleased showing hidden challenge data
type=unreleased — hidden challenge data returned without any access check.

No access control on who can query unreleased content. The parameter might as well not exist. In a real app this could mean exposing beta features, upcoming product data, or anything else that wasn't ready to go public.

10. Unsafe Consumption of APIs — NoSQL Injection

POST /api/login

Last one, and honestly it's a fun one if you've never seen it before. The login endpoint takes a JSON body and passes it straight into a MongoDB query — no sanitisation. MongoDB supports special query operators like $ne (not equal) directly in JSON. So instead of a password string, you can pass a query object that matches anything.

Normal login body:

{
  "username": "admin",
  "password": "somepassword"
}

NoSQL injection — bypass the password entirely:

{
  "username": "admin",
  "password": {"$ne": null}
}
NoSQL injection in /api/login — password field replaced with MongoDB $ne operator
Password swapped for a MongoDB operator — query matches any password, login succeeds.

MongoDB evaluates the query, finds the admin user whose stored password is "not equal to null" — which is always true — and hands back a valid token. Full admin access without ever knowing the actual password.

Some thoughts after going through all of this

The thing that stuck with me most doing this wasn't any individual bug — it was how often the same core mistake shows up. The server just... trusts you. Trusts that the username you passed is yours. Trusts that the fields in your JSON are the only ones that matter. Trusts that the URL you gave it is safe to fetch. Trusts that your password is a string and not a query object. That misplaced trust is basically the whole list.

I also found it interesting how many of these are invisible from the outside unless you're actively looking. BOLA in particular — from a user's perspective everything seems to work fine. You'd never notice it unless someone was in Burp swapping parameter values around. Which is exactly why it keeps showing up in real-world pentests so often.

If you're getting into API testing and haven't run through DVAPI yet, it's worth a few hours of your time. Set it up locally, open Burp, and just poke at things. Half the fun is finding the bugs before reading anyone else's writeup — so maybe close this tab and try it yourself first.