Skip to main content
🧠Educationalintermediate6 min read
β€’

Writeup: Bypassing Broken Access Controls in a Legacy Web App

Technical writeup of identifying and exploiting broken access control vulnerabilities in a legacy web application during a penetration test.

access controlweb securitypenetration testingOWASP

Writeup: Bypassing Broken Access Controls in a Legacy Web App

Broken access control moved to #1 in the OWASP Top 10 (2021) for good reasonβ€”it's everywhere. This writeup details access control vulnerabilities discovered during a penetration test of a legacy web application.

Engagement Overview

Target: Custom-built inventory management system Age: ~10 years, minimal updates Stack: PHP/MySQL Scope: Web application penetration test Goal: Identify access control vulnerabilities

Reconnaissance

Application Structure

The application has three user roles:

  1. Admin: Full access to all features
  2. Manager: Department-level access
  3. User: View-only access to assigned inventory

Initial Observations

URL patterns suggested direct object references:

/inventory.php?item_id=1234
/reports.php?report_id=5
/users.php?user_id=42

No obvious authorization tokens in requestsβ€”just session cookies.

Vulnerability 1: Insecure Direct Object Reference (IDOR)

Discovery

Logged in as a standard user, I could view my assigned inventory items:

GET /inventory.php?item_id=1001

Changing the ID parameter:

GET /inventory.php?item_id=1002

Returned inventory item 1002, which belonged to a different department.

Exploitation

Wrote a simple script to enumerate all inventory items:

import requests

session = requests.Session()
# ... authentication ...

for item_id in range(1, 10000):
    response = session.get(f'https://target/inventory.php?item_id={item_id}')
    if 'Item not found' not in response.text:
        print(f'Found item: {item_id}')
        # Parse and save item data

Impact: Any authenticated user could view all inventory items across all departments.

Root Cause

The code retrieved items by ID without checking if the user had permission:

// Vulnerable code
$item_id = $_GET['item_id'];
$query = "SELECT * FROM inventory WHERE id = $item_id";
$result = mysqli_query($conn, $query);

No authorization check.

Remediation

// Fixed code
$item_id = $_GET['item_id'];
$user_dept = $_SESSION['department'];
$query = "SELECT * FROM inventory 
          WHERE id = ? AND department = ?";
// Use prepared statements and verify authorization

Vulnerability 2: Forced Browsing to Admin Functions

Discovery

Explored the application's JavaScript files and found references to admin endpoints:

// admin.js
const adminEndpoints = {
    userManagement: '/admin/users.php',
    systemConfig: '/admin/config.php',
    exportAll: '/admin/export.php'
};

Exploitation

Accessing these endpoints as a standard user:

GET /admin/users.php

Expected: 403 Forbidden or redirect to login Actual: Full user management interface!

The application checked if a user was logged in but not their role.

Impact

Any authenticated user could:

  • View all user accounts
  • Create new admin users
  • Modify existing user permissions
  • Export all system data

Root Cause

// Vulnerable code
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}
// Missing role check!
// Admin functionality follows...

Remediation

// Fixed code
session_start();
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}
if ($_SESSION['role'] !== 'admin') {
    http_response_code(403);
    die('Access denied');
}

Vulnerability 3: Parameter Manipulation for Privilege Escalation

Discovery

When creating a new user account, I intercepted the request:

POST /register.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=newuser&email=test@test.com&password=Test123!

The response set a cookie: role=user

Exploitation

Modified the registration request to include a role parameter:

POST /register.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=hacker&email=hacker@test.com&password=Test123!&role=admin

Result: Account created with admin privileges!

Impact

Anyone could create admin accounts through the public registration form.

Root Cause

// Vulnerable code
$username = $_POST['username'];
$email = $_POST['email'];
$password = $_POST['password'];
$role = isset($_POST['role']) ? $_POST['role'] : 'user';  // User-controlled!

$query = "INSERT INTO users (username, email, password, role) 
          VALUES (?, ?, ?, ?)";

Remediation

// Fixed code
$username = $_POST['username'];
$email = $_POST['email'];
$password = $_POST['password'];
$role = 'user';  // Always set server-side for registration

// Only allow role specification through admin interface with proper checks

Discovery

Examining cookies after login:

session_id=abc123def456
user_id=42
department=5

The user_id and department were stored in unsigned cookies!

Exploitation

Modified cookies:

user_id=1
department=1

Refreshing the page showed data for user ID 1 (an admin) and department 1 (executive).

Impact

Complete compromise of any user account by manipulating cookie values.

Root Cause

The application trusted client-supplied cookies for authorization decisions instead of server-side session data.

Remediation

  • Store authorization data server-side only
  • Use signed/encrypted cookies if client-side data is necessary
  • Validate session integrity on every request

Vulnerability 5: Missing Function-Level Access Control

Discovery

While testing as a manager, I tried accessing the delete functionality:

GET /inventory.php?action=delete&item_id=1001

My role shouldn't allow deletions, but the item was deleted.

Systematic Testing

Tested all actions across all roles:

ActionAdminManagerUserExpectedActual
Viewβœ“βœ“βœ“βœ“βœ“
Createβœ“βœ“βœ—βœ“βœ“ (User can!)
Modifyβœ“βœ“βœ—βœ“βœ“ (User can!)
Deleteβœ“βœ—βœ—βœ“βœ“ (Everyone!)
Exportβœ“βœ“βœ—βœ“βœ“ (User can!)

Impact

All users had all privileges regardless of assigned role.

Root Cause

The UI hid buttons based on role, but the server didn't enforce permissions:

// Vulnerable code
if ($_GET['action'] == 'delete') {
    $query = "DELETE FROM inventory WHERE id = ?";
    // No role check!
}

Remediation

// Fixed code
if ($_GET['action'] == 'delete') {
    if (!hasPermission($_SESSION['role'], 'delete')) {
        http_response_code(403);
        die('Permission denied');
    }
    $query = "DELETE FROM inventory WHERE id = ?";
}

Summary of Findings

VulnerabilitySeverityCVSSStatus
IDOR on inventoryHigh7.5Fixed
Forced browsing to adminCritical9.1Fixed
Parameter manipulationCritical9.8Fixed
Cookie manipulationCritical9.1Fixed
Missing function-level ACHigh8.1Fixed

Lessons for Developers

Access Control Principles

  1. Deny by default β€” Explicitly grant access, don't deny
  2. Server-side enforcement β€” Never trust client-side controls
  3. Check every request β€” Authorization on every function
  4. Validate ownership β€” Verify user owns the resource
  5. Log access attempts β€” Detect attack patterns

Implementation Checklist

  • All endpoints have authorization checks
  • Role checks happen server-side
  • Direct object references are validated
  • Session data stored server-side only
  • Admin functions protected by role
  • API endpoints have same controls as UI
  • Authorization tested in security testing

Tools Used

  • Burp Suite (interception, manipulation)
  • Custom Python scripts (enumeration)
  • Browser developer tools (cookie inspection)

Need a penetration test for your web application? Contact us: m1k3@msquarellc.net

Found this helpful? Share it:

Need Help With This?

Have questions about implementing these security practices? Let's discuss your specific needs.

Get in Touch

More in Educational

Explore more articles in this category.

Browse 🧠 Educational

Related Articles