Scanner group

Access control

Every scanner in this group works from one kind of evidence: a request sent by a principal who should have been refused, and the response that came back. The principal is anonymous, a second user, a regular user instead of an admin, or a client sending a field it was never meant to control. The scanner then reads the body, because a 200 on its own proves nothing.

6 scanners4 DAST2 SAST+ 1 DAST/SAST pairup to Critical

How this group works

Four principals, three resources, one loop. Each step below is one scanner from the group sending its characteristic request. Watch which principal sends it, what comes back, and what in the response turns a reply into a finding.

Anonymous no token · public anon key User B · regular Authorization: Bearer B User A · admin Bearer A · owns tasks/42 /api/tasks/42 owner: user-a /api/admin/users path matches /admin POST /api/Users writes a profile row Request → response → decision the shared question A principal who should be refused sends the request that a more privileged principal is allowed to send. The scanner reads the reply: status and body. Two SAST scanners ask the same question of the code instead of the server. See step 6. IDOR scanner · cross-user BOLA GET /api/tasks/42 Bearer B 200 {"id":42,"owner":"user-a",...} FLAG: body carries User A's data guard: anonymous got 401 for the same id, so the object is not simply public Privilege escalation scanner GET /api/admin/users Bearer B 200 {"data":[ ...48 users ]} FLAG: /admin path answered 200 to a regular user differential variant: admin 48 records vs regular 3, both over 50 B, admin more than 1.5x larger Mass assignment scanner POST /api/Users {"role":"admin"} 201 {"status":"success","data":{"role":"admin"}} FLAG: 2xx and role=admin found in the body searched anywhere in the JSON tree, depth 8; role:"customer" would not count Body param fuzzer · User A's own request PATCH /api/tasks/42 {"title":"' OR '1'='1' --"} 500 SQLITE_ERROR: near "OR": syntax error FLAG: database error pattern in the reply the field came from a captured write, with the rest of the body left intact Route auth analyzer · RLS policy analyzer · RLS deep tasks/[id]/route.ts no getUser() before the query migration.sql tasks: no ENABLE ROW LEVEL SECURITY GET /rest/v1/tasks apikey: anon → 200 [rows] FLAG: the guard is missing in code and live a DAST and a SAST finding of this pair confirm each other Shared rule: a 200 alone never makes a finding. The body has to show it: the other user's data, the reflected field, the larger record count, the error text, the missing guard.

1Three principals and three resources. Each scanner picks the principal that should be refused and sends the request a more privileged principal is allowed to send. The decision is always made on the response body, never on the status alone.

2IDOR. User B asks for task 42, which belongs to User A. A 200 is only a finding if the body carries User A's data, and only if an anonymous request for the same id was refused; otherwise the object is public and nothing is wrong.

3Privilege escalation. The regular user requests a path that matches an admin indicator. A 200 is the finding. With two accounts the scanner also compares the admin's and the regular user's replies to the same GET, and flags only when the admin's is more than 1.5 times larger and holds more records.

4Mass assignment. A bare body with one escalation field goes to every POST, PUT and PATCH endpoint. The finding needs a 2xx and the same field with the same value somewhere in the reply, however deep the envelope.

5Body param fuzzer. A write that User A really sent during the crawl is replayed with one field replaced. A database error pattern, a verbatim reflection, a 5xx on a wrong type, or a __proto__ key accepted without a 400 each make a finding.

6The static side. Route auth tracing reads the handler for a guard and follows it to its definition. The RLS policy analyzer reads the migrations; the RLS deep scanner asks the live table with the public anon key. When a DAST and a SAST finding of a paired category land on the same app, correlation raises both.

Scanners in this group

User B →GET /api/tasks/42200{ owner: "user-a", …}

IDOR and cross-user BOLA

Why an unauthenticated swap is only a lead, harvesting User A's real ids, the two guards that kill false positives, and mutation read-back.

DASTMediumHighCriticalidor-scanner.md
rowsauth.uid() = user_id

Row Level Security

The anon key is public, so a policy is the only thing between the internet and the table. Migration parsing on one side, live anon and cross-user queries on the other.

SASTDASTCriticalrls-*.md
route.tstrpc.ts:42auth.getUser() ✓

Route auth tracing

Guard names on word boundaries, lookups that are not authentication, and a language server following the guard to its definition.

SASTHighroute-auth-analyzer.md
User B/api/adminrole=adminrpc/fn

Privilege escalation

Eight probes from the regular user's session: admin tables and routes, role self-elevation, the admin-vs-regular differential with its 1.5x rule, replaying the admin's writes, and RPC calls.

DASTMediumCriticalprivilege-escalation-scanner.md
POST {"role":"admin"}201 {"data":{"role":"admin" }}

Mass assignment

One escalation field per request to every write endpoint, a reflection check that walks the whole JSON envelope, and the bare-body limit that keeps one Juice Shop challenge out of reach.

DASTHighmass-assignment-scanner.md
{"item":"mug", "qty":"' OR '1'='1", "__proto__":{…}}

Body param fuzzer

Captured authenticated writes replayed with one JSON field swapped: SQL error patterns, verbatim reflection, 5xx on a wrong type, and a prototype-pollution key accepted without a 400.

DASTMediumHighCriticalbody-param-fuzzer.md

At a glance

ScannerWhat it looks atWhat it reportsSeverityNeeds
IDOR scannerEndpoints with ids; the ids User A owns, harvested during the crawlUser B reading or mutating User A's object; unauthenticated swaps as leads onlyMedium to CriticalURL; credentials, a second account for cross-user; --probe-writes for the mutation path
RLS policy analyzerSupabase migration SQLTables without Row Level Security or with permissive policiesCriticalRepository
RLS deep scannerLive Supabase REST with the anon key and both sessionsAnonymous and cross-user reads and writes the policies should blockCriticalURL; Supabase URL and anon key found in the JS; credentials
Route auth analyzerEvery API route file and its handler; the guard's definition via LSPRoutes missing authentication or input validationHighRepository; a language server for guard tracing (optional)
Privilege escalation scannerAdmin-looking tables and routes, protected endpoints, writes captured from User A, User A's owned ids, RPC names from the bundleRegular user reaching admin data, elevating its own role, replaying admin writes, writing another user's row, calling RPC functionsMedium to CriticalURL; credentials; second account for the admin-vs-regular tests; Supabase for the table, row and RPC tests
Mass assignment scannerEvery POST, PUT and PATCH endpoint in the inventoryAn escalation field accepted and reflected in the replyHighURL only; --probe-writes when the bundle yields only GETs; session headers when logged in
Body param fuzzerJSON bodies of successful writes captured during the authenticated crawlSQL error, reflected XSS, type confusion 5xx, prototype pollution key acceptedMedium to CriticalURL; credentials (the authenticated crawl supplies the requests)

Why this class matters

Why it matters

Access control bugs do not need a payload. The request is well formed, the server is working as written, and the only thing wrong is who was allowed to send it. Privilege escalation turns a free account into full admin access: every record, every setting, every delete, and new admin accounts for persistence. Mass assignment lets a client promote itself, zero a price or mark an order paid by adding a field. An IDOR hands one user another user's data by changing a number. None of these show up in a crash log.

Real-world breaches

Microsoft Exchange, ProxyLogon (2021). CVE-2021-26855 let attackers authenticate as the Exchange server, escalate to admin and install web shells across tens of thousands of organisations. GitHub (2012). Egor Homakov used a Rails mass assignment flaw to attach his SSH key to the Rails core repository by setting user_id, gaining commit access. HackerOne (2015). A mass assignment flaw allowed adding yourself as a member of any bug bounty program by manipulating group membership parameters.