Documentation

# Broken Access Control (IDOR) Attacks

CRITICAL SEVERITY OWASP TOP 10 #1 AUTHORIZATION

Broken Access Control Illustration
Broken Access Control Illustration

Imagine staying at a hotel where your room key card works on every door in the building - not just your room, but every other guest's room too. That's essentially what Broken Access Control (also called IDOR - Insecure Direct Object Reference) is on websites.

IDOR is currently the #1 vulnerability on the OWASP Top 10 list, and it's shockingly simple: a hacker just changes a number in a URL and suddenly they're looking at someone else's bank account, medical records, or private messages.

Simple Example: You visit yourbank.com/account?id=12345 to see your account. What if you just change it to yourbank.com/account?id=12346? In a vulnerable system, you'd see someone else's account!


# What is Broken Access Control? (In Simple Terms)

Every website has different pages and data that different users should be able to access. For example:

  • You should see your bank account
  • Your doctor should see their patient records
  • An admin should access admin tools

Access Control is the system that enforces these rules - it's the bouncer at the club checking IDs.

Broken Access Control happens when that bouncer falls asleep. The website checks if you're logged in, but doesn't check if you're authorized to access that specific data. So you can just change a number or ID in the URL and access things you shouldn't.

# Real-World Analogies

Your hotel room key should only open Room 305 (your room). But imagine if it opened Rooms 304, 306, 307 - every room on the floor! That's broken access control.

Imagine an office where employees can access filing cabinets, but there's no lock on individual drawers. You're supposed to only open Drawer A (your files), but nothing stops you from opening Drawer B, C, D (everyone else's files).

You give your valet ticket (#100) to pick up your car. But the valet doesn't check if ticket #100 is actually yours - they just hand over whatever car matches that number. So you could try tickets #101, #102, #103 and drive away with someone else's car.


# How Broken Access Control Works (The Simple Story)

Let's see how this plays out on a real website:

Scenario: Online Banking

You log into your bank account. The website shows you this URL:

https://mybank.com/account?user_id=42

You see your account balance: $5,000

Out of curiosity (or malicious intent), you change the URL to:

https://mybank.com/account?user_id=43

Instead of seeing an error or "Access Denied," you see someone else's account:

  • Account holder: John Smith
  • Balance: $125,000
  • Recent transactions
  • Social security number (last 4 digits)

# Types of Broken Access Control

# 1. Insecure Direct Object References (IDOR)

MOST COMMON CRITICAL

What it is: The most common type. The application uses user-supplied input (like an ID number) to directly access objects (database records, files) without verifying authorization.

Real-World Example - Social Media:

You view your private messages at:

https://socialmedia.com/messages?conversation_id=9876

You change it to:

https://socialmedia.com/messages?conversation_id=9877

Now you're reading someone else's private conversation!

# Vulnerable Code Example

[Flask - Vulnerable]
# Flask application - VULNERABLE!
@app.route('/user/profile')
def view_profile():
    user_id = request.args.get('id')

    # Gets user from database
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")

    # PROBLEM: No check if current_user is authorized to view this profile!
    return render_template('profile.html', user=user)
[Express - Vulnerable]
// Express.js - VULNERABLE!
app.get('/api/user/:id', (req, res) => {
    const userId = req.params.id;

    // Gets user from database
    const user = db.users.findById(userId);

    // PROBLEM: No authorization check!
    res.json(user);
});

How the Attack Works:

Attacker is logged in as User #100

Visits /user/profile?id=100 - sees own profile

Changes URL to /user/profile?id=101 - sees another user's profile

Writes script to loop through all IDs: 1, 2, 3... 10,000

Downloads entire user database in minutes

# 2. Missing Function Level Access Control

HIGH RISK PRIVILEGE ESCALATION

What it is: The application doesn't verify if a user has permission to perform a specific function or action.

Real-World Example - Admin Panel:

A regular user discovers the admin panel URL:

https://website.com/admin/dashboard

They shouldn't be able to access it, but the website only hides the link - it doesn't actually check permissions. So if they know (or guess) the URL, they can access admin functions.

# Vulnerable Code Example

// Express.js - VULNERABLE!
app.get('/admin/delete-user', (req, res) => {
    const userId = req.query.userId;

    // PROBLEM: No check if current user is an admin!
    db.users.delete(userId);

    res.send('User deleted');
});

# 3. Horizontal Privilege Escalation

MEDIUM RISK SAME-LEVEL ACCESS

What it is: Accessing resources belonging to another user at the same privilege level.

Example: User A accessing User B's private data (both are regular users)

User A (logged in) visits: /user/orders?user=A
Changes to: /user/orders?user=B
Now sees User B's order history

# 4. Vertical Privilege Escalation

CRITICAL RISK ADMIN ACCESS

What it is: A regular user gaining admin/elevated privileges.

Example: Regular user performing admin actions

POST /api/update-profile
{
  "username": "attacker",
  "role": "admin"  // Changed from "user" to "admin"
}

If not validated server-side, user becomes admin!

# 5. Path Traversal

FILE SYSTEM ACCESS

What it is: Manipulating file paths to access files outside the intended directory.

[Normal]
/download?file=invoice.pdf
[Attack]
/download?file=../../../etc/passwd
[Result]
Downloads server's password file instead of invoice

# Real-World Attack Scenarios

# Scenario 1: Facebook's IDOR Vulnerability (2019)

50M ACCOUNTS ACCESS TOKENS

The Attack:

Facebook had an IDOR vulnerability in their "View As" feature. An attacker could:

Use "View As" to see how their profile looks to others

Manipulate the access token in the process

Steal access tokens of other Facebook users

Impact:

  • 50 million accounts compromised
  • Attackers could fully take over accounts
  • Facebook forced logout of 90 million users as precaution

The Fix: Proper validation that access tokens belong to the requesting user.

# Scenario 2: Instagram Direct Messages IDOR (2020)

PRIVATE MESSAGES $30K BOUNTY

The Attack:

Normal endpoint: /api/messages?thread_id=ABC123
Attack: Changed thread_id to another value
Result: Read anyone's private Instagram messages

Researcher Found: By simply changing the thread ID parameter, they could access private direct messages between any Instagram users.

Impact:

  • Could have affected billions of users
  • Found by security researcher, reported through bug bounty
  • Researcher received $30,000 reward

# Scenario 3: Medical Records Breach

HIPAA VIOLATION 10K RECORDS

The Attack:

A healthcare portal used patient IDs in URLs:

https://hospital.com/records?patient_id=1001

How it Happened:

Patient logs in, sees their records at patient_id=1001

Changes URL to patient_id=1002, 1003, 1004...

Writes automated script to download all patient records

In one weekend, downloaded 10,000 patient medical records

Impact:

  • Massive HIPAA violation
  • Hospital fined millions
  • Personal health information exposed (diagnoses, medications, SSNs)

# Scenario 4: E-commerce Order Manipulation

FINANCIAL LOSS ORDER THEFT

The Attack:

Online store let users view orders:

https://shop.com/order?id=ORDER12345

Attack Steps:

User places order (ORDER12345) for $100 item

Changes URL to another order: ORDER12346

Finds another customer's order for $2,000 laptop

Changes shipping address to their own

Other customer's laptop ships to attacker's address

Impact:

  • Financial loss for company
  • Customer complaints and chargebacks
  • Legal liability

# How Hackers Exploit IDOR (Step-by-Step)

# Method 1: Manual Enumeration

BASIC TECHNIQUE

Hacker looks for URLs with IDs:

/profile?user_id=123
/document?doc_id=456
/api/messages?conversation=789

Change the ID and see what happens:

Original: /profile?user_id=123
Test: /profile?user_id=124
  • Vulnerable: Shows different user's profile
  • Secure: Shows error or redirects to own profile

If vulnerable, write a script to loop through all IDs.

# Method 2: Parameter Tampering

ADVANCED

Common Parameters to Test:

[User IDs]
user_id, userid, id, uid
account, account_id
[Resources]
order_id, invoice_id
file, filename, document
[Permissions]
role, permission, access_level

Testing Techniques:

?id=100 → ?id=101 → ?id=102
?invoice=INV-001 → INV-002 → INV-003
?doc=a1b2c3d4 → Try common/leaked GUIDs
?user_id=5 → ?user_id=-1 (might return admin)
?id=0 (might return all records)
?id=null
?id=admin

# Method 3: Mass Assignment

CRITICAL

The Vulnerability: Sending extra parameters that shouldn't be modifiable:

POST /api/update-profile
{
  "name": "John Doe",
  "email": "john@example.com"
}
POST /api/update-profile
{
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin",           // Added by attacker
  "is_verified": true,       // Added by attacker
  "account_balance": 999999  // Added by attacker
}

# Testing for IDOR Vulnerabilities

# Manual Testing Checklist

TESTING GUIDE

  • Account A (your account)
  • Account B (test victim account)

Look for:

  • URLs with IDs or references
  • API endpoints that return user data
  • File download/upload features
  • Any functionality that accesses resources

Log in as Account A, then try to access Account B's resources:

# Profile access
Account A logged in
Try accessing: /user/profile?id=<Account_B_ID>

# Document access
Account A logged in
Try accessing: /documents/view?doc=<Account_B_document>

# API endpoints
Account A logged in
Call: GET /api/orders/<Account_B_order_ID>
GET /api/user/5     # View user 5
PUT /api/user/5     # Update user 5
DELETE /api/user/5  # Delete user 5
POST /api/user/5/reset-password  # Reset user 5's password
?id=0
?id=-1
?id=999999999
?id=null
?id=admin
?id=<empty>
?id[]=1&id[]=2  # Array injection

# Automated Testing Tools

[Burp Suite]
1. Capture request in Burp Proxy
2. Send to Repeater
3. Change ID parameters
4. Use Intruder for automated testing with number ranges
[OWASP ZAP]
# Built-in active scanner detects some IDOR issues
# Fuzzer can test parameter variations
[Custom Python Script]
import requests

base_url = "https://example.com/api/user"
cookies = {"session": "your_session_cookie"}

# Test user IDs 1-1000
for user_id in range(1, 1001):
    response = requests.get(
        f"{base_url}/{user_id}",
        cookies=cookies
    )

    if response.status_code == 200:
        print(f"[VULNERABLE] Accessed user {user_id}")
        print(f"Data: {response.json()}")

# Prevention and Mitigation

# 1. Implement Proper Authorization Checks

PRIMARY DEFENSE

The Golden Rule: For every request, verify two things:

  1. Authentication: Is the user logged in?
  2. Authorization: Is this user allowed to access THIS specific resource?

Secure Code Examples:

[Python (Flask) - Secure]
from flask import abort

@app.route('/user/profile')
def view_profile():
    requested_user_id = request.args.get('id')
    current_user_id = session.get('user_id')

    # Authorization check
    if requested_user_id != current_user_id:
        abort(403, "Access denied")

    user = db.query(f"SELECT * FROM users WHERE id = {requested_user_id}")
    return render_template('profile.html', user=user)
[Node.js (Express) - Secure]
app.get('/api/orders/:orderId', async (req, res) => {
    const orderId = req.params.orderId;
    const currentUserId = req.session.userId;

    // Get order from database
    const order = await db.orders.findById(orderId);

    // Authorization check
    if (!order) {
        return res.status(404).json({ error: 'Order not found' });
    }

    if (order.userId !== currentUserId) {
        return res.status(403).json({ error: 'Access denied' });
    }

    res.json(order);
});
[PHP - Secure]
<?php
session_start();

$requested_user_id = $_GET['id'];
$current_user_id = $_SESSION['user_id'];

// Authorization check
if ($requested_user_id != $current_user_id) {
    http_response_code(403);
    die("Access denied");
}

$user = $db->query("SELECT * FROM users WHERE id = ?", [$requested_user_id]);
?>

# 2. Use Indirect References

OBFUSCATION

Instead of using actual database IDs in URLs, use random tokens or session-based references.

/download?file_id=12345
# Generate random token mapped to file
/download?token=a8f3k9m2p5q7

# Or use user's session to maintain mappings
Session['file_map'] = {
    'file1': 12345,
    'file2': 12346
}
/download?ref=file1
import secrets

# When user uploads file
file_token = secrets.token_urlsafe(32)
db.execute(
    "INSERT INTO files (id, user_id, token) VALUES (?, ?, ?)",
    [file_id, user_id, file_token]
)

# When user downloads
@app.route('/download')
def download_file():
    token = request.args.get('token')
    current_user = session.get('user_id')

    file = db.query(
        "SELECT * FROM files WHERE token = ? AND user_id = ?",
        [token, current_user]
    )

    if not file:
        abort(404)

    return send_file(file.path)

# 3. Implement Role-Based Access Control (RBAC)

RECOMMENDED

Define Clear Roles:

ROLES = {
    'user': ['read_own_profile', 'edit_own_profile'],
    'moderator': ['read_own_profile', 'edit_own_profile', 'delete_posts'],
    'admin': ['read_all_profiles', 'edit_all_profiles', 'delete_users']
}

def check_permission(user, action, resource):
    user_role = user.role

    # Check if role has permission for this action
    if action not in ROLES.get(user_role, []):
        return False

    # Additional checks for resource ownership
    if action.startswith('read_own_') or action.startswith('edit_own_'):
        return resource.owner_id == user.id

    return True

# Usage
@app.route('/user/profile/<int:user_id>')
def view_profile(user_id):
    current_user = get_current_user()
    target_user = db.users.get(user_id)

    # Check permission
    if user_id == current_user.id:
        action = 'read_own_profile'
    else:
        action = 'read_all_profiles'

    if not check_permission(current_user, action, target_user):
        abort(403)

    return render_template('profile.html', user=target_user)

# 4. Use UUIDs Instead of Sequential IDs

DEFENSE IN DEPTH

User ID: 1, 2, 3, 4, 5...
Easy to guess and enumerate
User ID: 7f8a3c2d-9e1b-4f5a-8c3d-2e9f7a1b4c5d
Impossible to guess
import uuid

# When creating user
user_id = str(uuid.uuid4())
db.execute(
    "INSERT INTO users (id, username, email) VALUES (?, ?, ?)",
    [user_id, username, email]
)

# URLs now look like:
# /user/profile?id=7f8a3c2d-9e1b-4f5a-8c3d-2e9f7a1b4c5d
# Attacker can't guess other valid IDs

# 5. Implement Access Control Lists (ACL)

COMPLEX PERMISSIONS

For complex permissions, use ACLs to explicitly define who can access what.

Database Schema:

CREATE TABLE access_control (
    id INT PRIMARY KEY,
    user_id INT,
    resource_type VARCHAR(50),
    resource_id INT,
    permission VARCHAR(20),
    UNIQUE(user_id, resource_type, resource_id, permission)
);

-- Examples:
-- User 5 can 'read' document 100
INSERT INTO access_control VALUES (1, 5, 'document', 100, 'read');

-- User 5 can 'write' document 100
INSERT INTO access_control VALUES (2, 5, 'document', 100, 'write');

-- User 7 can 'read' document 100 (shared document)
INSERT INTO access_control VALUES (3, 7, 'document', 100, 'read');

Implementation:

def check_access(user_id, resource_type, resource_id, permission):
    acl = db.query("""
        SELECT * FROM access_control
        WHERE user_id = ?
          AND resource_type = ?
          AND resource_id = ?
          AND permission = ?
    """, [user_id, resource_type, resource_id, permission])

    return acl is not None

# Usage
@app.route('/document/<int:doc_id>')
def view_document(doc_id):
    current_user_id = session.get('user_id')

    if not check_access(current_user_id, 'document', doc_id, 'read'):
        abort(403, "Access denied")

    document = db.documents.get(doc_id)
    return render_template('document.html', doc=document)

# 6. Deny by Default

SECURITY PRINCIPLE

# Start with deny
access_granted = False

# Explicitly grant access if conditions met
if user.is_admin or resource.owner_id == user.id:
    access_granted = True

if not access_granted:
    abort(403)
# Start with allow (DANGEROUS!)
access_denied = False

# Only deny in specific cases
if user.is_banned:
    access_denied = True

# Might miss cases where access should be denied

# Summary: What You Need to Remember

Broken Access Control (IDOR) is when a website doesn't properly check if you're authorized to access specific data. Attackers simply change ID numbers in URLs to access other users' private information.

The Simple Version:

  • What it is: Like a hotel key that opens every room, not just yours
  • Why it's dangerous: Access anyone's private data by changing a number in the URL - no hacking skills needed
  • How to prevent it: Always verify the current user is authorized to access the requested resource

Real-World Impact:

  • Facebook: 50 million accounts compromised (2019)
  • Instagram: Private messages exposed (2020)
  • Healthcare: Thousands of medical records leaked

Currently #1 on OWASP Top 10 - More prevalent than SQL injection or XSS!

# Quick Protection Checklist

For Website Owners & Developers:

DO Always verify authorization, not just authentication DO Check if current_user owns the requested resource DO Use indirect references (tokens) instead of database IDs DO Implement Role-Based Access Control (RBAC) DO Use UUIDs instead of sequential IDs (plus authorization checks) DO Deny access by default, explicitly grant when authorized DO Test with multiple user accounts

DON'T Trust user-supplied IDs without verification DON'T Assume authentication means authorization DON'T Expose internal database IDs in URLs DON'T Rely on hiding links/buttons for access control DON'T Use client-side checks alone

For Regular Users:

TIP Be cautious with apps that show user IDs in URLs - they might be vulnerable TIP If you accidentally see someone else's data, report it immediately TIP Use services with good security track records TIP Enable two-factor authentication - helps if account IDs leak


# Additional Resources


Last updated: November 2025