Prerequisites
- Target system, dependencies and environment configured.
Usage
Purpose
Broken access control sits at the top of the OWASP Top 10 for a reason: it's everywhere and it's high-impact. The app knows who you are but doesn't consistently check what you're allowed to touch. This skill covers the whole family — object-level (IDOR), function-level, and forced browsing — in a web-app context, and how to enforce authorisation properly.
The API-specific object-level case has its own deeper skill; this one takes the web-app view across all three axes.
When to use it
On any authenticated app. It's the class of bug you can find without special tools — just two accounts, a proxy, and patience.
The three axes
- Horizontal — reach another user's data at your own privilege level (your
/account/1002 → their /account/1001).
- Vertical — reach a higher privilege level (a normal user hitting
/admin/*).
- Function-level / forced browsing — the UI hides a capability but the endpoint still answers (the "delete" button is gone for you, the
DELETE route isn't).
Procedure
- Set up two accounts — a low-priv user A and, ideally, an admin B. Log in as A in your proxy and browse normally to collect real requests and IDs.
- Horizontal: take a request that returns your data, change the identifier, replay with A's session. Someone else's data back = IDOR.
curl -b "session=A" https://app.tld/api/account/1001
- Vertical: as A, request admin functionality directly by URL. The UI won't link it; the route may still work:
curl -b "session=A" https://app.tld/admin/users
- Function-level: replay an admin action captured from B's session, but with A's cookie. If it succeeds, the action isn't checking the caller's role:
curl -b "session=A" -X POST https://app.tld/api/users/55/promote
- Test parameter- and method-based bypasses: does adding
?admin=true, changing role in a body, or switching GET to POST slip past a check that only guards one shape?
- Check that logout and privilege changes take effect — an old session or token that still works after a role downgrade is its own access-control bug.
Cheatsheet
curl -b "s=A" https://app.tld/orders/1000
curl -b "s=A" https://app.tld/orders/1001
curl -b "s=A" https://app.tld/admin/
curl -b "s=A" https://app.tld/api/admin/config
curl -b "s=A" -X DELETE https://app.tld/api/users/42
curl -b "s=A" -X POST https://app.tld/api/users/42/role -d '{"role":"admin"}'
ffuf -b "s=A" -u https://app.tld/FUZZ -w paths.txt -mc 200,302
Reading the output
200 + data/action that belongs to someone else or to a higher role = broken access control. The successful status is the confirmation.
403/401 on the cross-boundary attempt = the check is present for that route — keep testing others, because coverage is usually inconsistent.
- A
GET blocked but the POST allowed (or vice versa) points to a check bolted onto one method, not the operation.
- An admin page returning its shell but no data may still leak structure; confirm whether the data endpoints behind it are also guarded.
The fix
Enforce authorisation server-side, on every request, from the session identity — never from a value the client can set, never from UI state.
- Default deny. A route with no explicit authorisation decision should reject, not allow.
- Check ownership/role at the point of data access, and prefer queries scoped to the caller so "forgot to check" can't return another tenant's rows.
- Centralise it — a policy layer, middleware, or framework guard — so new endpoints inherit enforcement instead of re-implementing (and re-forgetting) it.
- Don't rely on unguessable IDs, hidden menus, or disabled buttons; those are UX, not access control.
- Invalidate sessions and re-check permissions on privilege changes and logout.
Pitfalls
- Testing one axis. Teams that get horizontal right often miss vertical or function-level. Cover all three.
- Client-side "security". A greyed-out button proves nothing about the endpoint behind it.
- Only checking read. Write and delete are where the damaging IDORs live and where checks lapse most.
- Assuming a WAF or gateway handles it. Authorisation is business logic; it belongs in the app, per object.
References
- OWASP Top 10 A01:2021 Broken Access Control
- OWASP WSTG-ATHZ (Authorization Testing)
- CWE-284, CWE-639, CWE-285
Inputs
- Relevant source code, logs, network traces, or system specifications.
Outputs
- Analysis findings, security audit report, or generated code artifacts.
1---2name: idor-and-broken-access-control3description: Use when testing whether a web app enforces access control server-side — horizontal and vertical privilege checks, function-level auth, and forced browsing — plus the fix.4---5678## Prerequisites9- Target system, dependencies and environment configured.1011## Usage12### Purpose1314Broken access control sits at the top of the OWASP Top 10 for a reason: it's everywhere and it's high-impact. The app knows who you are but doesn't consistently check what you're allowed to touch. This skill covers the whole family — object-level (IDOR), function-level, and forced browsing — in a web-app context, and how to enforce authorisation properly.1516The API-specific object-level case has its own deeper skill; this one takes the web-app view across all three axes.1718### When to use it1920On any authenticated app. It's the class of bug you can find without special tools — just two accounts, a proxy, and patience.2122### The three axes2324- **Horizontal** — reach another user's data at your own privilege level (your `/account/1002` → their `/account/1001`).25- **Vertical** — reach a higher privilege level (a normal user hitting `/admin/*`).26- **Function-level / forced browsing** — the UI hides a capability but the endpoint still answers (the "delete" button is gone for you, the `DELETE` route isn't).2728### Procedure29301. Set up two accounts — a low-priv user A and, ideally, an admin B. Log in as A in your proxy and browse normally to collect real requests and IDs.312. **Horizontal:** take a request that returns your data, change the identifier, replay with A's session. Someone else's data back = IDOR.32 ```33 curl -b "session=A" https://app.tld/api/account/100134 ```353. **Vertical:** as A, request admin functionality directly by URL. The UI won't link it; the route may still work:36 ```37 curl -b "session=A" https://app.tld/admin/users38 ```394. **Function-level:** replay an admin action captured from B's session, but with A's cookie. If it succeeds, the action isn't checking the caller's role:40 ```41 curl -b "session=A" -X POST https://app.tld/api/users/55/promote42 ```435. Test **parameter- and method-based** bypasses: does adding `?admin=true`, changing `role` in a body, or switching `GET` to `POST` slip past a check that only guards one shape?446. Check that **logout and privilege changes take effect** — an old session or token that still works after a role downgrade is its own access-control bug.4546### Cheatsheet4748```bash49curl -b "s=A" https://app.tld/orders/100050curl -b "s=A" https://app.tld/orders/10015152curl -b "s=A" https://app.tld/admin/53curl -b "s=A" https://app.tld/api/admin/config5455curl -b "s=A" -X DELETE https://app.tld/api/users/4256curl -b "s=A" -X POST https://app.tld/api/users/42/role -d '{"role":"admin"}'5758ffuf -b "s=A" -u https://app.tld/FUZZ -w paths.txt -mc 200,30259```6061### Reading the output6263- **`200` + data/action that belongs to someone else or to a higher role** = broken access control. The successful status is the confirmation.64- **`403`/`401` on the cross-boundary attempt** = the check is present for that route — keep testing others, because coverage is usually inconsistent.65- **A `GET` blocked but the `POST` allowed** (or vice versa) points to a check bolted onto one method, not the operation.66- **An admin page returning its shell but no data** may still leak structure; confirm whether the data endpoints behind it are also guarded.6768### The fix6970Enforce authorisation **server-side, on every request, from the session identity** — never from a value the client can set, never from UI state.7172- Default deny. A route with no explicit authorisation decision should reject, not allow.73- Check ownership/role at the point of data access, and prefer queries scoped to the caller so "forgot to check" can't return another tenant's rows.74- Centralise it — a policy layer, middleware, or framework guard — so new endpoints inherit enforcement instead of re-implementing (and re-forgetting) it.75- Don't rely on unguessable IDs, hidden menus, or disabled buttons; those are UX, not access control.76- Invalidate sessions and re-check permissions on privilege changes and logout.7778### Pitfalls7980- **Testing one axis.** Teams that get horizontal right often miss vertical or function-level. Cover all three.81- **Client-side "security".** A greyed-out button proves nothing about the endpoint behind it.82- **Only checking read.** Write and delete are where the damaging IDORs live and where checks lapse most.83- **Assuming a WAF or gateway handles it.** Authorisation is business logic; it belongs in the app, per object.8485### References8687- OWASP Top 10 A01:2021 Broken Access Control88- OWASP WSTG-ATHZ (Authorization Testing)89- CWE-284, CWE-639, CWE-2859091## Inputs92- Relevant source code, logs, network traces, or system specifications.9394## Outputs95- Analysis findings, security audit report, or generated code artifacts.