Featured image of post Understanding Broken Access Control: Vulnerabilities, Examples, and Mitigations

Understanding Broken Access Control: Vulnerabilities, Examples, and Mitigations

An in-depth developer's guide to Broken Access Control (OWASP Top 10

Imagine you’re visiting a secure, high-tech corporate office building. You walk up to the reception desk, prove who you are with your ID, and receive a visitor badge. This badge grants you access to the main lobby and the visitor lounge.

Now, imagine if you could take the elevator to the penthouse, walk straight into the CEO’s office, and read confidential files on their desk simply because the elevator buttons weren’t restricted and the office door was left unlocked.

In the digital world, this failure is known as Broken Access Control.

Access control is the mechanism that ensures users cannot act outside of their intended permissions. When access control is broken, users can access resources or perform actions that they should not be authorized to access.

In the latest OWASP Top 10, Broken Access Control holds the infamous #1 spot as the most common and serious web application security risk. Let’s dive deep into how it works, explore real-world examples, and learn the rules to mitigate it.


Authentication vs. Authorization

Before we look at the vulnerabilities, we must clarify a common point of confusion:

  • Authentication (AuthN): “Who are you?” This is the process of verifying a user’s identity (e.g., logging in with a username and password, or using multi-factor authentication).
  • Authorization (AuthZ): “What are you allowed to do?” This is the process of verifying if an authenticated user has the permission to access a specific resource or execute a specific action.

Broken Access Control is fundamentally an authorization failure. The user has successfully logged in (Authentication works), but the system fails to restrict their actions based on their identity or role (Authorization fails).


The Core Types of Access Control

Understanding how access control is structured helps us identify where it breaks. Modern applications typically use a mix of these models:

  1. Discretionary Access Control (DAC): The owner of a resource decides who gets access. (e.g., sharing a Google Doc with a colleague).
  2. Mandatory Access Control (MAC): A central authority regulates access based on security levels. (e.g., military clearance levels).
  3. Role-Based Access Control (RBAC): Access is assigned to specific “roles” (e.g., Admin, Editor, Viewer), and users are assigned to these roles.
  4. Attribute-Based Access Control (ABAC): A highly flexible model where access is granted based on attributes (e.g., “Allow access only if the user is in the HR department AND the current time is between 9 AM and 5 PM”).

4 Common Broken Access Control Vulnerabilities

Let’s look at the most frequent access control failures that developers introduce, accompanied by vulnerable and secure code examples.

1. Insecure Direct Object References (IDOR)

An IDOR occurs when an application exposes a direct reference to an internal database object (like a database ID or file name) in a URL or API request, allowing an attacker to manipulate that reference to access another user’s data.

The Vulnerable Code (Node.js / Express)

Consider an endpoint that retrieves invoices for a logged-in user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Vulnerable: The app fetches invoice by ID directly from request parameters 
// without checking if the logged-in user owns that invoice.
app.get('/api/invoices/:invoiceId', async (req, res) => {
    const { invoiceId } = req.params;
    
    try {
        const invoice = await db.query('SELECT * FROM invoices WHERE id = ?', [invoiceId]);
        if (!invoice) {
            return res.status(404).send('Invoice not found');
        }
        res.json(invoice);
    } catch (err) {
        res.status(500).send('Database error');
    }
});
  • The Exploit: An attacker logs in as User A (Invoice ID 1001). By changing the request to /api/invoices/1002, they can view User B’s invoice because the code only retrieves the invoice by ID without checking ownership.

The Mitigated Code

To fix IDOR, the application must verify that the authenticated user has permission to access the requested resource:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Secure: Check ownership before returning the resource
app.get('/api/invoices/:invoiceId', async (req, res) => {
    const { invoiceId } = req.params;
    const currentUserId = req.user.id; // Assumes authentication middleware populated req.user
    
    try {
        const invoice = await db.query('SELECT * FROM invoices WHERE id = ?', [invoiceId]);
        
        if (!invoice) {
            return res.status(404).send('Invoice not found');
        }
        
        // Verify ownership (Authorization check)
        if (invoice.user_id !== currentUserId) {
            return res.status(403).send('Unauthorized: You do not own this invoice');
        }
        
        res.json(invoice);
    } catch (err) {
        res.status(500).send('Database error');
    }
});

2. Privilege Escalation

Privilege escalation occurs when a user obtains access to resources or functions that are reserved for users with higher privileges (Vertical) or similar privileges (Horizontal).

  • Vertical Privilege Escalation: A regular user accesses administrative functions (e.g., changing their role to “Admin”).
  • Horizontal Privilege Escalation: A user accesses another user’s private account or resources (essentially IDOR).

The Vulnerable Code

Many applications trust the client-side user interface to hide admin buttons, but fail to protect the underlying API endpoint on the server:

1
2
3
4
5
6
7
// Vulnerable: Endpoint is public and does not check the user's role
app.post('/api/admin/delete-user', async (req, res) => {
    const { userIdToDelete } = req.body;
    
    await db.query('DELETE FROM users WHERE id = ?', [userIdToDelete]);
    res.send('User deleted successfully');
});
  • The Exploit: Even if the “Delete User” button is hidden in the React/Angular UI for non-admin users, an attacker can capture the API request using a tool like Postman or Burp Suite and send the request manually.

The Mitigated Code

Implement server-side middleware to enforce role checks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Secure: Middleware checks user role before executing administrative tasks
const isAdmin = (req, res, next) => {
    if (req.user && req.user.role === 'Admin') {
        return next();
    }
    return res.status(403).send('Access Denied: Administrator role required');
};

app.post('/api/admin/delete-user', isAdmin, async (req, res) => {
    const { userIdToDelete } = req.body;
    
    await db.query('DELETE FROM users WHERE id = ?', [userIdToDelete]);
    res.send('User deleted successfully');
});

3. Path Traversal (Directory Traversal)

Path traversal occurs when an application accepts file paths from user input without validation, allowing attackers to navigate up the directory structure (using ../) and read sensitive files from the server’s local file system.

The Vulnerable Code

Consider a profile picture endpoint that reads a file name from the query parameters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const fs = require('fs');
const path = require('path');

// Vulnerable: Directly appending user input to target directory path
app.get('/view-image', (req, res) => {
    const filename = req.query.file;
    const baseDir = path.join(__dirname, 'public', 'images');
    
    const filePath = path.join(baseDir, filename);
    
    fs.readFile(filePath, (err, data) => {
        if (err) return res.status(404).send('File not found');
        res.end(data);
    });
});
  • The Exploit: An attacker sends a request like /view-image?file=../../../../etc/passwd to bypass the public/images directory constraint and read the system’s password file.

The Mitigated Code

Sanitize user input and verify that the resolved path stays within the intended directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
const path = require('path');

app.get('/view-image', (req, res) => {
    const filename = req.query.file;
    const baseDir = path.resolve(__dirname, 'public', 'images');
    
    // Resolve the absolute path of the target file
    const filePath = path.resolve(baseDir, filename);
    
    // Ensure the resolved path starts with the base directory path
    if (!filePath.startsWith(baseDir)) {
        return res.status(403).send('Forbidden: Access outside allowed directory');
    }
    
    fs.readFile(filePath, (err, data) => {
        if (err) return res.status(404).send('File not found');
        res.end(data);
    });
});

4. Missing Method-Level Access Control

APIs often offer multiple HTTP methods (GET, POST, PUT, DELETE) for a single resource. A common mistake is securing only some of these methods while leaving others unprotected.

For example, a developer might write a route controller that permits access to GET /api/reports only for authenticated users, but forgets to protect POST /api/reports or DELETE /api/reports, allowing unauthorized users to create or delete reports.


Crucial Mitigation Rules

Securing access control requires a structured approach across your entire software development lifecycle. Follow these golden rules:

1. Enforce “Deny by Default” (Default Deny)

When designing your routing and API structure, explicitly deny access to all endpoints by default. Only expose endpoints to the public or to specific roles through active configuration.

2. Never Trust the Client

Never rely on client-side routing, hidden UI elements, or obfuscation to protect sensitive features. Every request arriving at the server must be assumed hostile and verified.

3. Use Indirect References (UUIDs)

Instead of auto-incrementing integers (like 1, 2, 3) for resource IDs, use cryptographically secure identifiers like UUIDs (Universally Unique Identifiers).

  • Predictable: https://example.com/api/users/42 (Easy to guess user 43)
  • Unpredictable: https://example.com/api/users/d883b632-4752-4a57-bc09-5a507871b6ba (Note: UUIDs do not replace authorization checks, but they prevent bulk-guessing and scraping attacks).

4. Write Unit and Integration Tests for Authorization

Make access control test-driven. Write tests that specifically try to access admin and user resources using guest or lower-privileged accounts.

1
2
3
4
5
6
7
8
// Example Test Concept
it('should return 403 Forbidden when a member tries to access admin settings', async () => {
    const response = await request(app)
        .get('/api/admin/settings')
        .set('Authorization', `Bearer ${memberToken}`);
        
    expect(response.status).toBe(403);
});

5. Log and Monitor Access Violations

Log all access control failures on the server side. If a user tries to access a resource they do not own or a privilege they do not have, log this event as a warning. Frequent authorization failures from a single IP address can indicate an active scanner or attacker attempting to exploit IDOR vulnerabilities.


Summary

Broken Access Control is a severe but highly preventable vulnerability. By adopting a deny-by-default stance, implementing strict server-side checks for every single API method and parameter, and avoiding client-side security assumptions, you can protect your application from unauthorized access and safeguard your users’ sensitive data.

Secure by design, build with caution!

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy