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.

· 3 min read

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 .js files. Search for "/api/ in the browser's sources or with grep over what you downloaded.
  • Brute-forcing routes and versions: /api/v1/, /api/v2/, /api/internal/. An old version may lack the new checks.
  • Methods: what GET does not allow, maybe PUT, PATCH or DELETE does. Try OPTIONS to 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/me answers with "role":"user", the field is called role.
  • 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 /admin redirects you to the login, check whether /admin/users or /api/admin/users answer 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: /admin or X-Custom-IP-Authorization sometimes 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 200 with an error inside. Filter on words (-fr "not found") instead of status code.
  • The IDOR works on GET but not on PUT: 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.

Practise it on real machines

Hack The Box machines where this technique is used. Each ▶ jumps to the minute of the IppSec video where it shows up.

IDOR 6 machines

Mass assignment 5 machines

API enumeration 15 machines

See all 15 machines →

Spotted a mistake or something missing? Tell us