Skip to main content

Broken Access Control

Broken Access Control is the #1 vulnerability in the OWASP Top 10 2021. It occurs when applications fail to properly enforce restrictions on what authenticated users are allowed to do, enabling attackers to access unauthorized functionality and data. This vulnerability can lead to unauthorized information disclosure, modification, or destruction of data, and performing business functions outside user privileges.

What is Broken Access Control?

Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to:
  • Unauthorized data access: Viewing other users’ information
  • Privilege escalation: Gaining admin rights without authorization
  • Insecure Direct Object References (IDOR): Manipulating references to access objects
  • Missing function level access control: Accessing admin functions as regular user
  • Metadata manipulation: Modifying JWT tokens, cookies, or hidden fields

Common Scenarios

  1. Horizontal Privilege Escalation: User A accesses User B’s data
  2. Vertical Privilege Escalation: Regular user gains admin privileges
  3. IDOR Vulnerabilities: Changing ID parameters to access other resources
  4. Missing Authorization Checks: APIs without proper access validation
  5. Forced Browsing: Accessing admin pages by guessing URLs

How the Attack Works

The DVWA Broken Access Control module simulates a user profile viewing system where users should only be able to view their own profiles. The system demonstrates how various flawed access control mechanisms can be bypassed.

Application Interface

<h2>User Profile Access</h2>
<form action="#" method="GET">
    <p>
        View user profile by ID: 
        <input type="text" name="user_id" value="1">
        <input type="submit" value="View Profile" name="action">
    </p>
</form>
```sql

The form allows viewing user profiles by ID - the question is whether proper authorization is enforced.

### Access Logging

The module includes comprehensive logging to track access attempts:

```sql
CREATE TABLE bac_log (
    id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT(6) NULL,
    target_id INT(6) NULL,
    ip_address VARCHAR(50) NULL,
    action VARCHAR(50) NULL,
    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)

Security Level Breakdown

Low Security

Vulnerability: Cookie-based access control that can be manipulated.
<?php
// Get current user's ID  
$query = "SELECT user_id, role FROM users WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
$row = ($result && mysqli_num_rows($result) > 0) ? mysqli_fetch_assoc($result) : array('user_id' => 0, 'role' => '');
$current_user_id = intval($row['user_id']);
$role = $row['role'];

$html = "";
if (isset($_GET['action']) && isset($_GET['user_id'])) {
    if (!preg_match('/^\d+$/', $_GET['user_id'])) {
        $html .= "<p>Invalid user ID format. Please enter a number.</p>";
    } else {
        $id = intval($_GET['user_id']);
        
        // Check if user exists first
        $check_query = "SELECT user_id FROM users WHERE user_id = $id";
        $check_result = mysqli_query($GLOBALS["___mysqli_ston"], $check_query);
        $user_exists = ($check_result && mysqli_num_rows($check_result) > 0);
        
        if (!$user_exists) {
            $html .= "<p>No user found with ID: {$id}</p>";
        } else {
            // "Secure" check that's still vulnerable
            if (isset($_COOKIE['user_id'])) {
                $cookie_id = intval($_COOKIE['user_id']);
                
                if ($id == $cookie_id) {
                    // Access granted
                    $query = "SELECT first_name, last_name, user_id, avatar FROM users WHERE user_id = $id;";
                    $result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
                    
                    if ($result && mysqli_num_rows($result) > 0) {
                        $row = mysqli_fetch_assoc($result);
                        $html .= "
                            <div class=\"profile-info\">
                                <h3>User Profile</h3>
                                <p>User ID: {$row['user_id']}</p>
                                <p>Name: {$row['first_name']} {$row['last_name']}</p>
                                <p>Avatar: {$row['avatar']}</p>
                                <!-- Hint: Cookies can be modified by users... -->
                            </div>";
                    }
                } else {
                    $html .= "<p>Access denied. You can only view your own profile.</p>";
                }
            } else {
                $html .= "<p>Access denied. No user_id cookie found.</p>";
            }
        }
        
        // Log access attempts
        $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
        $target_id = $user_exists ? $id : 0;
        $log_query = "INSERT INTO bac_log (user_id, target_id, ip_address) VALUES 
                    ({$current_user_id}, {$target_id}, '{$ip}')";
        mysqli_query($GLOBALS["___mysqli_ston"], $log_query);
    }
}
?>
```javascript

**How it works**:
- Checks if `user_id` cookie exists
- Compares cookie value with requested user ID
- Only allows access if they match
- **Fatal Flaw**: Cookies are client-side and can be modified!

**Exploitation**:

1. **Using Browser DevTools**:
   ```javascript
   // Open browser console (F12)
   // View current cookies
   document.cookie
   
   // Set user_id cookie to target user
   document.cookie = "user_id=1; path=/"
   
   // Access URL
   // http://target/vulnerabilities/bac/?user_id=1&action=View+Profile
  1. Using cURL:
    # View user ID 1's profile
    curl "http://target/vulnerabilities/bac/?user_id=1&action=View+Profile" \
      --cookie "user_id=1; PHPSESSID=your_session_id"
    
    # View user ID 2's profile  
    curl "http://target/vulnerabilities/bac/?user_id=2&action=View+Profile" \
      --cookie "user_id=2; PHPSESSID=your_session_id"
    

3. **Automated Enumeration**:
   ```python
   import requests
   
   session = requests.Session()
   # Login first to get valid session
   session.post('http://target/login', data={'username': 'user', 'password': 'pass'})
   
   # Enumerate all user profiles
   for user_id in range(1, 10):
       # Set cookie to match target user
       session.cookies.set('user_id', str(user_id))
       
       # Request profile
       response = session.get(
           'http://target/vulnerabilities/bac/',
           params={'user_id': user_id, 'action': 'View Profile'}
       )
       
       if 'User Profile' in response.text:
           print(f"[+] Successfully accessed user {user_id}")
           # Extract data from response
Weaknesses:
  • Client-side access control (cookies)
  • No server-side session validation
  • Trusts user-supplied data
  • Simple comparison allows easy bypass
  • SQL injection vulnerability (no prepared statements)

Medium Security

Vulnerability: Weak token-based authentication.
<?php
// Get current user's ID
$query = "SELECT user_id FROM users WHERE user = '" . dvwaCurrentUser() . "';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
$current_user_id = ($result && mysqli_num_rows($result) > 0) ? mysqli_fetch_assoc($result)['user_id'] : 0;

$html = "";
if (isset($_GET['action']) && isset($_GET['user_id'])) {
    if (!preg_match('/^\d+$/', $_GET['user_id'])) {
        $html .= "<p>Invalid user ID format. Please enter a number.</p>";
    } else {
        $id = $_GET['user_id'];
        $user_exists = false;
        
        // Check if user exists first
        $check_query = "SELECT user_id FROM users WHERE user_id = '$id'";
        $check_result = mysqli_query($GLOBALS["___mysqli_ston"], $check_query);
        $user_exists = ($check_result && mysqli_num_rows($check_result) > 0);
        
        // "Secure" check that's easily bypassed
        if (isset($_GET['token']) && $_GET['token'] == 'user_token') {
            if ($user_exists) {
                $query = "SELECT first_name, last_name, user_id, avatar FROM users WHERE user_id = '$id';";
                $result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
                
                if ($result && mysqli_num_rows($result) > 0) {
                    $row = mysqli_fetch_assoc($result);
                    $html .= "
                        <div class=\"profile-info\">
                            <h3>User Profile</h3>
                            <p>User ID: {$row['user_id']}</p>
                            <p>Name: {$row['first_name']} {$row['last_name']}</p>
                            <p>Avatar: {$row['avatar']}</p>
                            <!-- Hint: This token check isn't very secure... -->
                        </div>";
                }
            } else {
                $html .= "<p>No user found with ID: {$id}</p>";
            }
        } else {
            $html .= "<p>Access denied. Valid token required. <!-- Try using token=user_token --></p>";
        }
        
        // Log access attempts
        $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
        $target_id = $user_exists ? $id : 0;
        $log_query = "INSERT INTO bac_log (user_id, target_id, ip_address) VALUES 
                    ({$current_user_id}, {$target_id}, '{$ip}')";
        mysqli_query($GLOBALS["___mysqli_ston"], $log_query);
    }
}
?>
```text

**How it works**:
- Requires a `token` parameter
- Checks if `token` equals the static value `'user_token'`
- **Fatal Flaw**: Token is the same for all users and visible in HTML comments!

**Exploitation**:

1. **Simple URL Manipulation**:
http://target/vulnerabilities/bac/?user_id=1&action=View+Profile&token=user_token

2. **Automated Access**:
   ```python
   import requests
   
   session = requests.Session()
   session.post('http://target/login', data={'username': 'user', 'password': 'pass'})
   
   # Access any user profile with static token
   for user_id in range(1, 10):
       response = session.get(
           'http://target/vulnerabilities/bac/',
           params={
               'user_id': user_id,
               'action': 'View Profile',
               'token': 'user_token'  # Same token for everyone!
           }
       )
       
       if 'User Profile' in response.text:
           print(f"[+] Accessed user {user_id} profile")
Weaknesses:
  • Static, predictable token
  • Same token for all users
  • Token visible in HTML comments
  • No user-specific validation
  • Still has SQL injection vulnerability
  • No prepared statements

High Security

Improvement: Session-based access control with prepared statements.
<?php
// Get current user's ID with prepared statement
$query = "SELECT user_id, role FROM users WHERE user = ? LIMIT 1";
$stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
$currentUser = dvwaCurrentUser();
mysqli_stmt_bind_param($stmt, "s", $currentUser);

mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user_info = ($result && mysqli_num_rows($result) > 0) ? mysqli_fetch_assoc($result) : ['user_id' => 0, 'role' => ''];
$current_user_id = intval($user_info['user_id']);
$role = $user_info['role'];
mysqli_stmt_close($stmt);

$html = "";
if (isset($_GET['action']) && isset($_GET['user_id'])) {
    if (!preg_match('/^\d+$/', $_GET['user_id'])) {
        $html .= "<p>Invalid user ID format. Please enter a number.</p>";
    } else {
        $id = intval($_GET['user_id']);

        // Check if user exists using prepared statement
        $check_query = "SELECT user_id FROM users WHERE user_id = ? LIMIT 1";
        $check_stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $check_query);
        mysqli_stmt_bind_param($check_stmt, "i", $id);
        mysqli_stmt_execute($check_stmt);
        mysqli_stmt_store_result($check_stmt);
        $user_exists = (mysqli_stmt_num_rows($check_stmt) > 0);
        mysqli_stmt_close($check_stmt);

        if (!$user_exists) {
            $html .= "<p>No user found with ID: {$id}</p>";
        } else {
            // "Secure" session-based check (but vulnerable to session fixation)
            if (isset($_SESSION['user_id'])) {
                $session_id = intval($_SESSION['user_id']);

                if ($id == $session_id) {
                    // Access granted - using prepared statement
                    $query = "SELECT first_name, last_name, user_id, avatar FROM users WHERE user_id = ?";
                    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
                    mysqli_stmt_bind_param($stmt, "i", $id);
                    mysqli_stmt_execute($stmt);
                    $result = mysqli_stmt_get_result($stmt);

                    if ($result && mysqli_num_rows($result) > 0) {
                        $row = mysqli_fetch_assoc($result);
                        $html .= "
                            <div class=\"profile-info\">
                                <h3>User Profile</h3>
                                <p>User ID: " . htmlspecialchars($row['user_id'], ENT_QUOTES, 'UTF-8') . "</p>
                                <p>Name: " . htmlspecialchars($row['first_name'], ENT_QUOTES, 'UTF-8') . " " .
                            htmlspecialchars($row['last_name'], ENT_QUOTES, 'UTF-8') . "</p>
                                <p>Avatar: " . htmlspecialchars($row['avatar'], ENT_QUOTES, 'UTF-8') . "</p>
                                <!-- Hint: Session management is better, but still vulnerable... -->
                            </div>";
                    }
                    mysqli_stmt_close($stmt);
                } else {
                    $html .= "<p>Access denied. You can only view your own profile.</p>";
                }
            } else {
                $html .= "<p>Access denied. No user_id in session.</p>";
            }
        }

        // Log access attempts with prepared statement
        $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
        $target_id = $user_exists ? $id : 0;

        $log_query = "INSERT INTO bac_log (user_id, target_id, ip_address) VALUES (?, ?, ?)";
        $log_stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $log_query);
        mysqli_stmt_bind_param($log_stmt, "iis", $current_user_id, $target_id, $ip);
        mysqli_stmt_execute($log_stmt);
        mysqli_stmt_close($log_stmt);
    }
}

// Set initial session if not exists
if (!isset($_SESSION['user_id'])) {
    $_SESSION['user_id'] = $current_user_id;
}
?>
```bash

**Improvements**:
- Uses server-side session storage
- Prepared statements prevent SQL injection
- Output encoding with `htmlspecialchars()`
- Input validation and type casting
- Comprehensive logging

**Remaining Vulnerability** - Session Fixation:

The code sets `$_SESSION['user_id']` if not present, but doesn't validate if this is legitimate:

```php
if (!isset($_SESSION['user_id'])) {
    $_SESSION['user_id'] = $current_user_id;
}
Exploitation: An attacker could potentially manipulate the session to set a different user ID, though this requires additional session vulnerabilities. Additional Issue: The session check only validates that the requested user ID matches the session user ID. While better than cookies, sessions can still be hijacked or fixated.

Impossible Security

Improvement: Comprehensive access control with multiple security layers.
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

$html = "";
$id = 0;
$current_user_id = 0;

if (isset($_GET['action']) && isset($_GET['user_id'])) {
    // 1. Input Validation
    if (!preg_match('/^\d+$/', $_GET['user_id'])) {
        $html .= "<p>Invalid user ID format. Please enter a number.</p>";
    } else {
        $id = intval($_GET['user_id']);

        // 2. Get Current User with Prepared Statement
        $query = "SELECT user_id, role FROM users WHERE user = ? LIMIT 1";
        $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
        if ($stmt) {
            $current_user = dvwaCurrentUser();
            mysqli_stmt_bind_param($stmt, "s", $current_user);
            mysqli_stmt_execute($stmt);
            $result = mysqli_stmt_get_result($stmt);

            if ($result && mysqli_num_rows($result) > 0) {
                $row = mysqli_fetch_assoc($result);
                $current_user_id = intval($row['user_id']);
                $user_role = $row['role'];
                mysqli_stmt_close($stmt);
                
                // 3. Check if target user exists
                $user_exists = false;
                $check_query = "SELECT user_id FROM users WHERE user_id = ? LIMIT 1";
                $check_stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $check_query);
                if ($check_stmt) {
                    mysqli_stmt_bind_param($check_stmt, "i", $id);
                    mysqli_stmt_execute($check_stmt);
                    mysqli_stmt_store_result($check_stmt);
                    $user_exists = (mysqli_stmt_num_rows($check_stmt) > 0);
                    mysqli_stmt_close($check_stmt);
                    
                    if (!$user_exists) {
                        $html .= "<p>No user found with ID: {$id}</p>";
                        logAccessAttempt($current_user_id, $id, 'non_existent_user_access');
                    } else if (isRateLimitExceeded($current_user_id)) {
                        // 4. Rate Limiting Check
                        $html .= "<p>Too many requests. Please try again later.</p>";
                    } else {
                        // 5. Access Control Check
                        $can_access = false;
                        if ($current_user_id === $id) {
                            $can_access = true; // Users can always view their own profile
                        }
                        // Admin check commented out - even stricter
                        // elseif ($user_role === 'admin') {
                        //     $can_access = true;
                        // }

                        if ($can_access) {
                            // Secure Data Retrieval with additional authorization in query
                            $query = "SELECT first_name, last_name, user_id, avatar 
                                        FROM users 
                                        WHERE user_id = ? 
                                        AND (user_id = ? OR ? = 'admin')
                                        LIMIT 1";

                            $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);

                            if ($stmt) {
                                mysqli_stmt_bind_param($stmt, "iis", $id, $current_user_id, $user_role);
                                mysqli_stmt_execute($stmt);
                                $result = mysqli_stmt_get_result($stmt);

                                if ($result && mysqli_num_rows($result) > 0) {
                                    $row = mysqli_fetch_assoc($result);

                                    // Output Encoding
                                    $html .= "
                                    <div class=\"profile-info\">
                                      <h3>User Profile</h3>
                                      <p>User ID: " . htmlspecialchars($row['user_id'], ENT_QUOTES, 'UTF-8') . "</p>
                                      <p>Name: " . htmlspecialchars($row['first_name'], ENT_QUOTES, 'UTF-8') . " " .
                                                htmlspecialchars($row['last_name'], ENT_QUOTES, 'UTF-8') . "</p>
                                      <p>Avatar: " . htmlspecialchars($row['avatar'], ENT_QUOTES, 'UTF-8') . "</p>
                                    </div>";

                                    // Log Successful Access
                                    logAccessAttempt($current_user_id, $id, 'view_profile_success');
                                } else {
                                    $html .= "<p>Access denied. Insufficient privileges.</p>";
                                    logSecurityEvent('access_denied', $id, $current_user_id);
                                }
                                mysqli_stmt_close($stmt);
                            }
                        } else {
                            $html .= "<p>Access denied. Insufficient privileges.</p>";
                            logSecurityEvent('unauthorized_access', $id, $current_user_id);

                            // Security Monitoring
                            checkForSuspiciousActivity($current_user_id, $id);
                        }
                    }
                }
            }
        }
    }
}

// Helper function: Rate limiting
function isRateLimitExceeded($user_id) {
    $timeframe = 300; // 5 minutes
    $max_attempts = 10;

    $query = "SELECT COUNT(*) as attempt_count 
              FROM bac_log 
              WHERE user_id = ? 
              AND timestamp > DATE_SUB(NOW(), INTERVAL ? SECOND)";

    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
    if ($stmt) {
        mysqli_stmt_bind_param($stmt, "ii", $user_id, $timeframe);
        mysqli_stmt_execute($stmt);
        $result = mysqli_stmt_get_result($stmt);
        $row = mysqli_fetch_assoc($result);
        mysqli_stmt_close($stmt);
        return $row['attempt_count'] >= $max_attempts;
    }
    return false;
}

// Helper function: Logging
function logAccessAttempt($user_id, $target_id, $action) {
    $target_id = intval($target_id);
    $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
    
    $query = "INSERT INTO bac_log (user_id, target_id, ip_address, action) 
              VALUES (?, ?, ?, ?)";
    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
    if ($stmt) {
        mysqli_stmt_bind_param($stmt, "iiss", $user_id, $target_id, $ip, $action);
        mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);
    }
}

// Helper function: Security event logging
function logSecurityEvent($action, $target_id, $user_id, $details = '') {
    $target_id = intval($target_id);
    $ip = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR'];
    
    $query = "INSERT INTO bac_log (user_id, target_id, ip_address, action, details) 
              VALUES (?, ?, ?, ?, ?)";
    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
    if ($stmt) {
        mysqli_stmt_bind_param($stmt, "iisss", $user_id, $target_id, $ip, $action, $details);
        mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);
    }
}

// Helper function: Suspicious activity detection
function checkForSuspiciousActivity($user_id, $target_id) {
    $timeframe = 3600; // 1 hour
    $threshold = 5;

    $query = "SELECT COUNT(*) as attempt_count 
              FROM bac_log 
              WHERE user_id = ? 
              AND timestamp > DATE_SUB(NOW(), INTERVAL ? SECOND) 
              AND action = 'unauthorized_access'";

    $stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
    if ($stmt) {
        mysqli_stmt_bind_param($stmt, "ii", $user_id, $timeframe);
        mysqli_stmt_execute($stmt);
        $result = mysqli_stmt_get_result($stmt);
        $row = mysqli_fetch_assoc($result);
        mysqli_stmt_close($stmt);

        if ($row['attempt_count'] >= $threshold) {
            // Log high-severity security event
            logSecurityEvent(
                'suspicious_activity',
                $target_id,
                $user_id,
                'Multiple unauthorized access attempts detected'
            );
        }
    }
}
?>
```bash

**Security Features**:

1. **Input Validation**: Regex and type casting
2. **Prepared Statements**: All database queries parameterized
3. **Output Encoding**: `htmlspecialchars()` on all output
4. **Rate Limiting**: Max 10 requests per 5 minutes
5. **Access Control**: Strict user ownership validation
6. **Defense in Depth**: Authorization checked in SQL query too
7. **Comprehensive Logging**: All access attempts logged
8. **Security Monitoring**: Detects repeated unauthorized access
9. **Error Handling**: Graceful failure modes
10. **Least Privilege**: Users can only access own data (admin disabled)

**Key Design Principles**:
- **Deny by default**: Access denied unless explicitly granted
- **Multiple layers**: Authorization checked in code AND database
- **Detailed logging**: Full audit trail
- **Anomaly detection**: Automatic detection of suspicious patterns
- **Rate limiting**: Prevents brute force enumeration

## Defense Strategies

### 1. Implement Proper Authorization

```php
class AccessControl {
    private $user;
    
    public function __construct($user) {
        $this->user = $user;
    }
    
    public function canAccessResource($resourceId, $resourceType) {
        // Get resource
        $resource = $this->getResource($resourceId, $resourceType);
        
        if (!$resource) {
            return false;
        }
        
        // Check ownership
        if ($resource['owner_id'] === $this->user['id']) {
            return true;
        }
        
        // Check role-based permissions
        if ($this->user['role'] === 'admin') {
            return true;
        }
        
        // Check shared access
        if ($this->hasSharedAccess($resourceId, $this->user['id'])) {
            return true;
        }
        
        return false;
    }
}

// Usage
$acl = new AccessControl($currentUser);
if (!$acl->canAccessResource($_GET['id'], 'profile')) {
    http_response_code(403);
    die('Access denied');
}

2. Use Indirect Object References

// BAD: Direct reference
$profileId = $_GET['profile_id'];
$profile = getProfile($profileId);

// GOOD: Indirect reference with mapping
$referenceMap = [
    'abc123' => ['user_id' => 5, 'profile_id' => 42],
    'def456' => ['user_id' => 8, 'profile_id' => 73]
];

$reference = $_GET['ref'];
if (!isset($referenceMap[$reference])) {
    die('Invalid reference');
}

$mapping = $referenceMap[$reference];
if ($mapping['user_id'] !== $currentUser['id']) {
    die('Access denied');
}

$profile = getProfile($mapping['profile_id']);
```bash

### 3. Implement Resource-Based Access Control

```php
function checkResourceAccess($userId, $resourceId, $action) {
    // Get resource
    $resource = getResource($resourceId);
    
    // Get user's permissions on this specific resource
    $permissions = getResourcePermissions($resourceId, $userId);
    
    // Check if user has required permission
    if (in_array($action, $permissions)) {
        return true;
    }
    
    // Check group permissions
    $userGroups = getUserGroups($userId);
    foreach ($userGroups as $group) {
        $groupPermissions = getResourcePermissions($resourceId, null, $group);
        if (in_array($action, $groupPermissions)) {
            return true;
        }
    }
    
    return false;
}

4. Centralized Authorization Middleware

class AuthorizationMiddleware {
    public function handle($request, $next) {
        $user = $request->user();
        $resource = $request->route('resource');
        $action = $request->method();
        
        // Map HTTP methods to actions
        $actionMap = [
            'GET' => 'read',
            'POST' => 'create',
            'PUT' => 'update',
            'DELETE' => 'delete'
        ];
        
        $requiredPermission = $actionMap[$action] ?? 'read';
        
        // Check authorization
        if (!$this->authorize($user, $resource, $requiredPermission)) {
            return response()->json(['error' => 'Forbidden'], 403);
        }
        
        return $next($request);
    }
    
    private function authorize($user, $resource, $permission) {
        // Implement authorization logic
        // Check user roles, permissions, resource ownership, etc.
        return AuthorizationService::check($user, $resource, $permission);
    }
}
```bash

## Testing Techniques

### 1. Manual IDOR Testing

```bash
# Test sequential ID manipulation
curl "http://target/api/user/1/profile"
curl "http://target/api/user/2/profile"
curl "http://target/api/user/3/profile"

# Test with different users
curl -H "Authorization: Bearer user1_token" "http://target/api/user/2/profile"

# Test parameter manipulation  
curl "http://target/api/profile?user_id=1"
curl "http://target/api/profile?user_id=2"

2. Automated Testing

import requests

def test_idor(base_url, auth_token, test_range=100):
    """Test for IDOR vulnerabilities"""
    
    headers = {'Authorization': f'Bearer {auth_token}'}
    vulnerable_endpoints = []
    
    for resource_id in range(1, test_range):
        # Test different endpoints
        endpoints = [
            f'/api/user/{resource_id}/profile',
            f'/api/document/{resource_id}',
            f'/api/order/{resource_id}'
        ]
        
        for endpoint in endpoints:
            response = requests.get(f'{base_url}{endpoint}', headers=headers)
            
            # Check if we got data we shouldn't have access to
            if response.status_code == 200:
                data = response.json()
                # Verify if this resource belongs to current user
                if not belongs_to_current_user(data):
                    vulnerable_endpoints.append({
                        'endpoint': endpoint,
                        'resource_id': resource_id,
                        'data': data
                    })
                    print(f"[!] IDOR found: {endpoint}")
    
    return vulnerable_endpoints
```sql

### 3. Burp Suite Testing

1. **Autorize Extension**:
   - Install Autorize extension
   - Configure low-privilege user token
   - Browse as high-privilege user
   - Autorize replays requests with low-privilege token
   - Identifies access control issues

2. **Manual Testing with Repeater**:
   - Capture legitimate request
   - Send to Repeater
   - Modify user IDs, resource IDs
   - Change authentication tokens
   - Analyze responses

## Best Practices Summary

1. **Deny by default**: Require explicit authorization
2. **Server-side enforcement**: Never trust client-side checks
3. **Validate ownership**: Check user owns requested resource
4. **Use indirect references**: Map UUIDs to internal IDs
5. **Implement RBAC**: Role-based access control
6. **Log all access**: Comprehensive audit trails
7. **Rate limiting**: Prevent enumeration attacks
8. **Defense in depth**: Multiple authorization layers
9. **Prepared statements**: Prevent SQL injection
10. **Output encoding**: Prevent XSS
11. **Input validation**: Validate all user input
12. **Least privilege**: Grant minimum necessary access
13. **Regular audits**: Test authorization regularly
14. **Centralized logic**: Single authorization system
15. **Monitor for attacks**: Detect suspicious patterns

## Real-World Impact

### Case Studies

**Facebook (2018)**: Photo API IDOR allowed access to private photos by manipulating photo IDs. Affected 6.8 million users.

**Instagram (2019)**: Profile modification endpoint allowed changing any user's profile by manipulating user ID parameter.

**Uber (2016)**: Rider ID manipulation allowed access to trip history of any user.

**Venmo (2019)**: Transaction IDs were sequential, allowing enumeration of all transactions.

## References

- [OWASP Top 10 2021 - A01 Broken Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
- [OWASP Testing Guide - Authorization Testing](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/02-Testing_for_Bypassing_Authorization_Schema)
- [CWE-639: Authorization Bypass Through User-Controlled Key](https://cwe.mitre.org/data/definitions/639.html)
- [OWASP IDOR Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html)

Build docs developers (and LLMs) love