API Testing — What the API Doesn't Want You to Find
March 20, 2026, 5:16 p.m.
Before doing this lab, I thought API testing meant reading documentation and calling endpoints properly.
Then I started poking at what the API didn't document.
What API testing actually is
Every dynamic website is built on APIs. When you click a button, fill a form, or load a page — somewhere underneath, an HTTP request hits an endpoint that returns data or triggers an action.
API testing is about finding what those endpoints do that they weren't supposed to do. Not just the documented surface — the hidden one.
Step 1: Recon
Before you can attack anything, you need to know what exists.
The first reflex is to look for documentation. APIs are often documented at predictable paths:
/api/swagger/index.html/openapi.json
If the docs are there, read them. If they're not, you look for them anyway — outdated docs left exposed are a goldmine.
The second reflex is to browse the application with Burp intercepting everything. JavaScript files are particularly useful: they often contain references to API endpoints that the front-end never directly exposes in the UI.
Step 2: Identifying endpoints and HTTP methods
Once you have a list of endpoints, you don't just call them the way the front-end does. You try every HTTP method.
An endpoint like /api/users/update might accept PATCH. But does it
also accept DELETE? PUT? GET? Each method can expose a different
behavior — sometimes one the developer didn't intend to expose at all.
Burp Intruder has a built-in HTTP verbs list. You load the endpoint, set the method as the injection point, and let it cycle through. What comes back tells you a lot.
Step 3: Mass assignment — the interesting one
This is where it got concrete for me.
Mass assignment happens when a framework automatically binds request
parameters to fields on an internal object. The developer exposes a
PATCH /api/users/ endpoint that lets users update their username and
email. But the internal user object also has an isAdmin field.
If the framework binds everything it receives without filtering, you can
just... add isAdmin: true to your request body. And it works.
The way to find these hidden parameters is to look at what the API
returns. A GET /api/users/123 might expose the full user object,
including fields the PATCH endpoint was never meant to touch. Once you
see isAdmin in the response, you know what to try in the next request.
{
"username": "wiener",
"email": "wiener@example.com",
"isAdmin": true
}
If the app doesn't validate and sanitize what it binds, that's privilege escalation via a JSON body.
What I changed after this lab
Going through this made me look at my own Django project differently.
In Django REST Framework, mass assignment is the default behavior of
serializers — every field you declare is writable unless you explicitly
mark it as read_only. A field like is_staff on a user serializer is
writable unless you say otherwise.
The fix is simple but easy to forget:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email']
read_only_fields = ['is_staff', 'is_superuser']
Allowlist what can be updated. Blocklist what can't. Never trust the framework to do it for you by default.
Takeaway
API testing starts with assuming the documented surface is incomplete. The interesting bugs live in the endpoints nobody listed, the HTTP methods nobody tested, and the parameters nobody was supposed to know existed.
The attack isn't clever. It's methodical.
Part of a series on web security labs — written as I learn it, not after.