Introduction: Guarding the Gates of Your Application

Welcome back, future security champions! In our previous chapters, we laid the groundwork for understanding how attackers think and how to approach web security from a defensive standpoint. We’ve talked about the crucial difference between authentication (who you are) and authorization (what you’re allowed to do). Today, we’re diving deep into one of the most critical and widespread vulnerabilities: Broken Access Control.

Broken Access Control consistently ranks as the number one vulnerability in the OWASP Top 10 (2021). This means it’s the most common way attackers gain unauthorized access to data or functionality. Think of it like a castle where the guards check your ID at the gate (authentication), but once inside, there are no locks on the treasure room, or the guards for the treasury are missing (broken authorization).

In this chapter, you’ll learn what Broken Access Control truly means, explore common ways it manifests, and understand how an attacker can exploit it. We’ll then roll up our sleeves with practical, step-by-step examples to reproduce these vulnerabilities safely and, most importantly, learn how to prevent them in your applications. By the end, you’ll have a solid grasp of how to properly guard the gates and inner chambers of your web applications.

Core Concepts: Understanding Access Control and Its Flaws

Before we can fix broken access control, we need to truly understand what robust access control looks like.

Authentication vs. Authorization: A Quick Refresher

Remember this fundamental distinction:

  • Authentication: Verifies a user’s identity. (“Are you who you say you are?”) This usually involves usernames, passwords, multi-factor authentication (MFA), etc. Once authenticated, a user is known to the system.
  • Authorization: Determines what an authenticated user is allowed to do or access. (“Now that we know who you are, what can you see or touch?”) This is about permissions, roles, and policies.

Broken Access Control is exclusively about authorization failures. The user might be perfectly authenticated, but the system fails to check if they should have access to a particular resource or function.

Types of Access Control Failures

Access control vulnerabilities often fall into a few common categories:

  1. Vertical Privilege Escalation: A lower-privileged user gains access to functions or data reserved for higher-privileged users.
    • Example: A regular user accessing an admin dashboard or deleting another user’s account.
  2. Horizontal Privilege Escalation: A user gains access to resources belonging to another user of the same privilege level.
    • Example: User A viewing or modifying User B’s private profile information, even though both are “regular users.”
  3. Context-Dependent Access Control Failures: Access is granted or denied based on the state of the application or specific business logic, and these checks are bypassed.
    • Example: An attacker modifying an “approved” order, even though the business logic dictates it should be immutable.

The Attacker’s Mindset: Probing for Weaknesses

An attacker looking for broken access control will often try to:

  • Change IDs: Modify parameters in URLs, request bodies, or headers that refer to objects (e.g., userId=123 to userId=456, orderId=ABC to orderId=XYZ). This is often called Insecure Direct Object Reference (IDOR).
  • Guess URLs/Endpoints: Try to access known admin or sensitive API endpoints directly (e.g., /admin, /api/users/delete, /dashboard).
  • Modify Roles/Permissions: Tamper with hidden form fields, JSON requests, or cookie values that might indicate their role or permissions.
  • Bypass Workflow: Skip steps in a multi-step process to gain unauthorized access or perform actions out of sequence.

The core idea is to see if the server trusts the client too much. Does the server assume the client is only asking for what it’s allowed to have, or does it verify every request against the user’s actual permissions? A secure application never trusts the client.

Visualizing Access Control Checks

Let’s use a simple flowchart to illustrate how a secure application handles a request that requires authorization.

flowchart TD A[User sends Request] --> B{Is User Authenticated?} B -->|No| C[Redirect to Login / Deny] B -->|Yes| D[Identify User Role/Permissions] D --> E{Does User have Permission for Action/Resource?} E -->|No| F[Deny Access / Return Error] E -->|Yes| G[Perform Action / Return Resource] G --> H[Response to User]

Explanation:

  1. User sends Request: An authenticated user tries to access /api/profile/123.
  2. Is User Authenticated?: The server first checks if the user has a valid session/token. If not, they’re not even allowed to try to access the resource.
  3. Identify User Role/Permissions: If authenticated, the server retrieves the user’s identity and associated roles or granular permissions.
  4. Does User have Permission?: This is the critical authorization check.
    • For /api/profile/123, if the authenticated user’s ID is 456, the server must check: “Is user 456 allowed to view profile 123?”
      • If it’s their own profile (123 == 456), then yes.
      • If it’s another user’s profile, perhaps only an admin is allowed, or it’s simply forbidden.
  5. Deny Access / Return Error: If the user doesn’t have permission, access is denied.
  6. Perform Action / Return Resource: If authorized, the request proceeds.

The “Broken Access Control” vulnerability occurs when step E (the authorization check) is either missing, incorrect, or easily bypassed.

Step-by-Step Implementation: Building & Breaking Access Control

Let’s create a simple Node.js Express application to demonstrate Broken Access Control, specifically IDOR and vertical privilege escalation.

Setup Your Project

First, let’s create a new project. We’ll use Node.js (v20.x LTS as of 2026-01-04 is a stable choice) and Express (v4.x is standard), a popular web framework.

  1. Create a project directory:

    mkdir broken-access-control-demo
    cd broken-access-control-demo
    
  2. Initialize Node.js project:

    npm init -y
    

    This creates a package.json file.

  3. Install Express and a simple authentication helper:

    npm install express jsonwebtoken bcryptjs --save
    
    • express (v4.18.2 as of 2026-01-04): Our web framework.
    • jsonwebtoken (v9.0.2 as of 2026-01-04): For creating and verifying JWTs (JSON Web Tokens) for authentication.
    • bcryptjs (v2.4.3 as of 2026-01-04): For hashing passwords securely.
  4. Create index.js: This will be our main server file.

1. The Vulnerable Application: A User Profile and Admin Panel

Let’s create a backend that simulates user profiles and an admin panel, but without proper authorization checks.

index.js (Initial Vulnerable Code):

// index.js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
const PORT = 3000;
const JWT_SECRET = 'your_super_secret_jwt_key_that_should_be_in_env_vars'; // In real apps, use process.env.JWT_SECRET

app.use(express.json()); // For parsing application/json

// --- Mock Database (In-memory for simplicity) ---
const users = [
    { id: 101, username: 'alice', passwordHash: bcrypt.hashSync('password123', 10), role: 'user', data: 'Alice\'s secret data' },
    { id: 102, username: 'bob', passwordHash: bcrypt.hashSync('securepass', 10), role: 'user', data: 'Bob\'s private info' },
    { id: 201, username: 'admin', passwordHash: bcrypt.hashSync('adminpass', 10), role: 'admin', data: 'Admin\'s top secret dashboard data' },
];

// --- Helper function to find user ---
const findUserById = (id) => users.find(u => u.id === parseInt(id));
const findUserByUsername = (username) => users.find(u => u.username === username);

// --- Middleware for basic authentication (checks if JWT is valid) ---
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

    if (token == null) return res.sendStatus(401); // No token, Unauthorized

    jwt.verify(token, JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // Token invalid/expired, Forbidden
        req.user = user; // Attach user payload to request
        next();
    });
};

// --- ROUTES ---

// 1. User Login Route
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = findUserByUsername(username);

    if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
        return res.status(401).send('Invalid credentials');
    }

    // If credentials are valid, generate a JWT
    const accessToken = jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '1h' });
    res.json({ accessToken });
});

// 2. Vulnerable User Profile Route (IDOR)
app.get('/api/profile/:userId', authenticateToken, (req, res) => {
    const { userId } = req.params; // The ID from the URL
    const targetUser = findUserById(userId);

    if (!targetUser) {
        return res.status(404).send('User not found');
    }

    // !!! VULNERABILITY: No authorization check here !!!
    // Any authenticated user can access any profile by changing userId in the URL
    res.json({ id: targetUser.id, username: targetUser.username, data: targetUser.data });
});

// 3. Vulnerable Admin Dashboard Route (Vertical Privilege Escalation)
app.get('/api/admin/dashboard', authenticateToken, (req, res) => {
    // !!! VULNERABILITY: Only authenticates, but doesn't check if user is an admin !!!
    res.json({ message: `Welcome to the Admin Dashboard, ${req.user.username}!`, adminData: 'Critical system metrics and controls' });
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

Explanation of the Vulnerabilities:

  • /api/profile/:userId (IDOR):

    • The route uses authenticateToken to ensure some user is logged in.
    • However, it fetches userId directly from the URL (req.params.userId) and uses it to retrieve targetUser.
    • Crucially, it never checks if req.user.id (the ID of the logged-in user) matches targetUser.id (the ID of the requested profile).
    • This means if Alice (ID 101) is logged in, she can simply change the URL from /api/profile/101 to /api/profile/102 and view Bob’s data!
  • /api/admin/dashboard (Vertical Privilege Escalation):

    • This route also uses authenticateToken, so only logged-in users can access it.
    • However, it completely lacks a check to see if req.user.role is ‘admin’.
    • This means Alice (role ‘user’) can log in, get her token, and then use that token to access /api/admin/dashboard, gaining unauthorized access to admin functionality.

2. Reproducing the Vulnerabilities (Ethical Hacking Practice)

Now, let’s act like an ethical hacker and try to exploit these.

Start your server:

node index.js

You should see: Server running on http://localhost:3000

We’ll use curl or a tool like Postman/Insomnia for these steps.

Step 1: Log in as Alice (a regular user)

curl -X POST -H "Content-Type: application/json" -d '{"username": "alice", "password": "password123"}' http://localhost:3000/login

Expected Output: You’ll get a JSON response containing an accessToken. Copy this token! Example token (will be different for you): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTAxLCJ1c2VybmFtZSI6ImFsaWNlIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3MDQzOTU0NTIsImV4cCI6MTcwNDM5otha_some_random_string_here

Step 2: Access Alice’s Own Profile (Legitimate Access)

Use Alice’s token from Step 1.

curl -H "Authorization: Bearer YOUR_ALICE_TOKEN" http://localhost:3000/api/profile/101

Expected Output:

{"id":101,"username":"alice","data":"Alice's secret data"}

This is legitimate. Alice is accessing her own data.

Step 3: Exploit IDOR - Access Bob’s Profile as Alice!

Now, while still using Alice’s token, change the userId in the URL to Bob’s ID (102).

curl -H "Authorization: Bearer YOUR_ALICE_TOKEN" http://localhost:3000/api/profile/102

Expected Output:

{"id":102,"username":"bob","data":"Bob's private info"}

Aha! Alice, a regular user, was able to view Bob’s private information simply by changing a number in the URL. This is a classic Insecure Direct Object Reference (IDOR) vulnerability. The server authenticated Alice but failed to authorize her to view Bob’s specific profile.

Step 4: Exploit Vertical Privilege Escalation - Access Admin Dashboard as Alice!

Still using Alice’s token, try to access the admin dashboard.

curl -H "Authorization: Bearer YOUR_ALICE_TOKEN" http://localhost:3000/api/admin/dashboard

Expected Output:

{"message":"Welcome to the Admin Dashboard, alice!","adminData":"Critical system metrics and controls"}

Gotcha! Alice, a regular user, has successfully accessed the admin dashboard. This is a Vertical Privilege Escalation because the server only checked if any user was authenticated, not if the authenticated user had the ‘admin’ role required for this resource.

3. Preventing Broken Access Control: Implementing Proper Authorization

Now let’s fix these vulnerabilities. The solution is always the same: server-side authorization checks.

Modify index.js (Adding Secure Middleware and Logic):

We’ll create a new middleware for role-based access and update our routes.

// index.js (with fixes)
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const app = express();
const PORT = 3000;
const JWT_SECRET = 'your_super_secret_jwt_key_that_should_be_in_env_vars'; // In real apps, use process.env.JWT_SECRET

app.use(express.json()); // For parsing application/json

// --- Mock Database (In-memory for simplicity) ---
const users = [
    { id: 101, username: 'alice', passwordHash: bcrypt.hashSync('password123', 10), role: 'user', data: 'Alice\'s secret data' },
    { id: 102, username: 'bob', passwordHash: bcrypt.hashSync('securepass', 10), role: 'user', data: 'Bob\'s private info' },
    { id: 201, username: 'admin', passwordHash: bcrypt.hashSync('adminpass', 10), role: 'admin', data: 'Admin\'s top secret dashboard data' },
];

// --- Helper function to find user ---
const findUserById = (id) => users.find(u => u.id === parseInt(id));
const findUserByUsername = (username) => users.find(u => u.username === username);

// --- Middleware for basic authentication (checks if JWT is valid) ---
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

    if (token == null) return res.sendStatus(401); // No token, Unauthorized

    jwt.verify(token, JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // Token invalid/expired, Forbidden
        req.user = user; // Attach user payload (id, username, role) to request
        next();
    });
};

// --- NEW: Middleware for role-based authorization ---
const authorizeRoles = (roles) => {
    return (req, res, next) => {
        if (!req.user || !roles.includes(req.user.role)) {
            return res.status(403).send('Forbidden: Insufficient privileges'); // Not allowed
        }
        next(); // User has required role, proceed
    };
};

// --- ROUTES ---

// 1. User Login Route (no changes needed here)
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    const user = findUserByUsername(username);

    if (!user || !bcrypt.compareSync(password, user.passwordHash)) {
        return res.status(401).send('Invalid credentials');
    }

    // If credentials are valid, generate a JWT
    const accessToken = jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '1h' });
    res.json({ accessToken });
});

// 2. FIXED User Profile Route (IDOR Prevention)
app.get('/api/profile/:userId', authenticateToken, (req, res) => {
    const { userId } = req.params; // The ID from the URL
    const targetUser = findUserById(userId);

    if (!targetUser) {
        return res.status(404).send('User not found');
    }

    // !!! FIX FOR IDOR: Check if the authenticated user is authorized to view this profile !!!
    // Option A: Only allow users to view their OWN profile
    if (req.user.id !== targetUser.id) {
        // Option B (more complex): If you want admins to view any profile, you'd add:
        // if (req.user.role !== 'admin') { // This would allow admins to view any profile
        return res.status(403).send('Forbidden: You can only view your own profile');
    }

    // If authorized, proceed
    res.json({ id: targetUser.id, username: targetUser.username, data: targetUser.data });
});

// 3. FIXED Admin Dashboard Route (Vertical Privilege Escalation Prevention)
app.get('/api/admin/dashboard', authenticateToken, authorizeRoles(['admin']), (req, res) => {
    // !!! FIX FOR VERTICAL PRIVILEGE ESCALATION: Use authorizeRoles middleware !!!
    // This route will now only be accessible if req.user.role is 'admin'
    res.json({ message: `Welcome to the Admin Dashboard, ${req.user.username}!`, adminData: 'Critical system metrics and controls' });
});

// Start the server
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
});

Key Changes and Explanations:

  • authorizeRoles Middleware:
    • This new function takes an array of roles (e.g., ['admin'], ['admin', 'editor']).
    • It returns another middleware function that checks if req.user.role (which was attached by authenticateToken) is included in the roles array.
    • If not, it sends a 403 Forbidden response.
  • Fixed /api/profile/:userId:
    • Inside the route, after authenticating and finding the targetUser, we now have a crucial if statement: if (req.user.id !== targetUser.id).
    • This explicitly checks if the ID of the logged-in user matches the ID of the requested profile. If they don’t match, access is denied with a 403 Forbidden. This directly prevents IDOR.
  • Fixed /api/admin/dashboard:
    • We added authorizeRoles(['admin']) to the route’s middleware chain.
    • Now, a request to this route will first be authenticated, then authorizeRoles will check if req.user.role is ‘admin’. Only if both pass will the route handler execute.

4. Retesting the Fixes

Restart your server (Ctrl+C then node index.js).

Step 1: Log in as Alice again to get a new token (good practice, though old one might still work if not expired).

curl -X POST -H "Content-Type: application/json" -d '{"username": "alice", "password": "password123"}' http://localhost:3000/login

Copy your new accessToken.

Step 2: Try to Exploit IDOR (as Alice, trying to access Bob’s profile)

curl -H "Authorization: Bearer YOUR_ALICE_TOKEN" http://localhost:3000/api/profile/102

Expected Output:

Forbidden: You can only view your own profile

Success! Alice is now correctly prevented from viewing Bob’s profile.

Step 3: Try to Exploit Vertical Privilege Escalation (as Alice, trying to access Admin Dashboard)

curl -H "Authorization: Bearer YOUR_ALICE_TOKEN" http://localhost:3000/api/admin/dashboard

Expected Output:

Forbidden: Insufficient privileges

Success! Alice is now correctly prevented from accessing the admin dashboard.

Step 4: Log in as Admin and verify legitimate access

curl -X POST -H "Content-Type: application/json" -d '{"username": "admin", "password": "adminpass"}' http://localhost:3000/login

Copy the admin’s accessToken.

curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" http://localhost:3000/api/admin/dashboard

Expected Output:

{"message":"Welcome to the Admin Dashboard, admin!","adminData":"Critical system metrics and controls"}

The admin can still access the dashboard, as expected. Our fixes are working!

Mini-Challenge: Secure an “Order Details” Endpoint

You’ve done a great job understanding and fixing IDOR and vertical privilege escalation. Now, it’s your turn to apply what you’ve learned.

Challenge: Imagine our mock application also has an orders array:

// Add this to your mock database in index.js
const orders = [
    { id: 1, userId: 101, item: 'Laptop', status: 'pending' },
    { id: 2, userId: 102, item: 'Keyboard', status: 'shipped' },
    { id: 3, userId: 101, item: 'Mouse', status: 'delivered' },
    { id: 4, userId: 201, item: 'Server Rack', status: 'approved' }, // Admin order
];

Your task is to:

  1. Create a new route: GET /api/order/:orderId
  2. Make it vulnerable first: Allow any authenticated user to view any order by its orderId parameter.
  3. Reproduce the IDOR vulnerability: Log in as Alice, then try to view Bob’s order (order ID 2).
  4. Fix the vulnerability: Implement server-side authorization so that:
    • A regular user (like Alice) can only view their own orders.
    • An admin user can view any order.

Hint: You’ll need to combine the IDOR prevention logic from the profile route with the role-based logic from the admin route. Remember, always check the req.user.id against the userId associated with the requested orderId.

What to observe/learn: This challenge reinforces the pattern of identifying the authenticated user, identifying the target resource, and then applying a logical check to ensure the authenticated user is authorized for that specific resource.

Common Pitfalls & Troubleshooting

Even with a good understanding, access control can be tricky. Here are some common mistakes:

  1. Client-Side Only Enforcement: Never rely solely on frontend UI elements (like disabling buttons or hiding links) to enforce access control. An attacker can easily bypass client-side restrictions using browser developer tools or by directly making API calls. Always enforce authorization on the server-side.
  2. Missing Checks on All Endpoints: It’s easy to secure primary routes but forget about less obvious ones (e.g., a hidden API for data export, or a legacy endpoint). Every endpoint that accesses sensitive data or performs sensitive actions must have proper authorization checks.
  3. Incorrect Privilege Mapping: Assigning roles or permissions incorrectly, or having a flawed hierarchy. For instance, if an “editor” role implicitly grants “admin” access to certain functions by mistake.
  4. Overly Permissive Defaults: Some frameworks or libraries might default to allowing access unless explicitly denied. It’s safer to adopt a “deny by default” approach, where access is only granted if explicitly allowed.
  5. Caching Authorized Content: If your application caches responses, ensure that cached content is not served to unauthorized users. For example, if an admin’s dashboard is cached, a regular user should not be able to retrieve it from the cache. Use appropriate caching headers (Cache-Control: no-store) for sensitive data.
  6. Trusting Input Parameters: Always validate and sanitize all input, including IDs, roles, and other parameters that could be tampered with.

If you’re having trouble, check:

  • Is your authenticateToken middleware correctly populating req.user with the id and role?
  • Are your if conditions for authorization correct? Are you comparing the right user ID to the right resource owner ID?
  • Are you applying the correct authorization middleware (e.g., authorizeRoles) to the right routes?
  • Are you checking for both the authenticated user’s ID and their role when deciding access?

Summary: Building a Fortified Application

Congratulations! You’ve successfully navigated the complexities of Broken Access Control. Let’s recap the key takeaways:

  • Broken Access Control (OWASP A01) is the most critical web security vulnerability, allowing unauthorized access to resources and functions.
  • It’s an authorization issue, not an authentication one.
  • Common manifestations include IDOR (Insecure Direct Object References) and Vertical/Horizontal Privilege Escalation.
  • The attacker’s mindset involves changing IDs, guessing URLs, and tampering with parameters.
  • Prevention is achieved through rigorous server-side authorization checks.
    • Always verify that the authenticated user is allowed to perform the requested action on the specific resource.
    • Implement role-based access control (RBAC) or attribute-based access control (ABAC) where appropriate.
    • Never trust client-side input or UI for authorization enforcement.
    • Adopt a “deny by default” policy for access.

By consistently applying these principles, you’ll significantly strengthen your applications against one of the most prevalent and dangerous web vulnerabilities.

Next up, we’ll tackle another critical vulnerability: Security Misconfiguration, which often goes hand-in-hand with access control issues. Get ready to learn how to keep your application’s environment locked down!

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.