IDOR, mass assignment and APIs: the access control flaws scanners miss
Enumerate an API (Swagger, hidden routes, versions), find IDORs by changing IDs, become admin with mass assignment and test access control with two users.
No injection, no CVE: just an application that does not check whether what you ask for is yours. These are the most common logic flaws in real assessments, and they appear on machines such as Cap, TwoMillion or Academy. A scanner will not find them because it does not know which data you are supposed to see.
1. Enumerate the API
Before testing permissions you need the full map (API enumeration):
- Forgotten documentation:
/swagger,/swagger-ui.html,/api-docs,/openapi.json,/v2/api-docs,/graphql(with introspection). - The client-side JavaScript: API routes are usually in the
.jsfiles. Search for"/api/in the browser's sources or withgrepover what you downloaded. - Brute-forcing routes and versions:
/api/v1/,/api/v2/,/api/internal/. An old version may lack the new checks. - Methods: what
GETdoes not allow, maybePUT,PATCHorDELETEdoes. TryOPTIONSto see which ones it accepts.
ffuf -u http://target.htb/api/FUZZ -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt -mc all -fc 404
ffuf -u http://target.htb/api/v1/user/FUZZ -w actions.txt -X POST -H 'Content-Type: application/json' -d '{}' -mc all -fc 404
On TwoMillion, the API itself lists every route if you request /api/v1, admin ones included.
2. IDOR: change the identifier
An insecure direct object reference (IDOR) is when the application uses an identifier you control and does not check the object belongs to you:
GET /api/invoices/1043 → your invoice
GET /api/invoices/1042 → someone else's?
GET /download/5 → on Cap, /data/0 is another user's packet capture
Where to look: in the URL, the JSON body, headers and cookies, and in file names (/uploads/user_17.pdf). If identifiers are not numbers, check whether they are predictable: base64 of a number, an MD5 of the email, or a version 1 UUID (it has the time inside).
# Walk identifiers and filter by response size
ffuf -u http://target.htb/api/invoices/FUZZ -w <(seq 0 2000) -H 'Cookie: session=...' -fs 23
The systematic way: two users
Create two accounts (or a normal one and an admin one, if you have both) and use Burp's Autorize extension: you browse as the more privileged user and the extension replays every request with the other user's cookie. If the response is the same, there is an access control flaw.
3. Mass assignment
The server turns the JSON you send straight into an object and saves it. If the object has fields you are not supposed to touch (role, is_admin, balance), add them yourself (mass assignment):
POST /api/register HTTP/1.1
Content-Type: application/json
{"username":"pepe","password":"pepe123","email":"[email protected]","role":"admin","isAdmin":true}
How to guess the field names:
- Look at what the API returns: if
GET /api/meanswers with"role":"user", the field is calledrole. - Search the JavaScript and the documentation.
- Try variants (
admin,is_admin,isAdmin,roleid,role_id,type,group).
On Academy, the sign-up form has a hidden roleid=0 field; setting it to 1 gives you an admin account. On TwoMillion, PUT /api/v1/admin/settings/update accepts {"is_admin":1} from any authenticated user.
4. Other access control flaws
- Unprotected admin routes: if
/adminredirects you to the login, check whether/admin/usersor/api/admin/usersanswer anyway. - Client-side-only checks: a button hidden with CSS, or a role read from
localStorage. Send the request by hand. - Trusted headers:
X-Forwarded-For: 127.0.0.1,X-Original-URL: /adminorX-Custom-IP-Authorizationsometimes bypass IP- or path-based restrictions. - Tamperable tokens: if the session is a JWT or a cookie signed with a weak secret (cookie forging), the role is inside.
Common errors
- Every response has the same size: the application returns
200with an error inside. Filter on words (-fr "not found") instead of status code. - The IDOR works on
GETbut not onPUT: also try the format: if the API accepts both JSON and form data, one of the two code paths may lack the check. - The extra field is ignored: it may be nested (
{"user":{"role":"admin"}}) or take another shape (role[]=admin).
What next
As an admin, the application often offers dangerous features: uploading templates or plugins, running diagnostics, generating reports. Go through the web checklist again with the new permissions: what was out of reach before may hide a command injection or an SSTI.