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:
- 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.
- 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.”
- 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=123touserId=456,orderId=ABCtoorderId=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.
Explanation:
- User sends Request: An authenticated user tries to access
/api/profile/123. - 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.
- Identify User Role/Permissions: If authenticated, the server retrieves the user’s identity and associated roles or granular permissions.
- Does User have Permission?: This is the critical authorization check.
- For
/api/profile/123, if the authenticated user’s ID is456, the server must check: “Is user456allowed to view profile123?”- 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.
- If it’s their own profile (
- For
- Deny Access / Return Error: If the user doesn’t have permission, access is denied.
- 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.
Create a project directory:
mkdir broken-access-control-demo cd broken-access-control-demoInitialize Node.js project:
npm init -yThis creates a
package.jsonfile.Install Express and a simple authentication helper:
npm install express jsonwebtoken bcryptjs --saveexpress(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.
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
authenticateTokento ensure some user is logged in. - However, it fetches
userIddirectly from the URL (req.params.userId) and uses it to retrievetargetUser. - Crucially, it never checks if
req.user.id(the ID of the logged-in user) matchestargetUser.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/101to/api/profile/102and view Bob’s data!
- The route uses
/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.roleis ‘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.
- This route also uses
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:
authorizeRolesMiddleware:- 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 byauthenticateToken) is included in therolesarray. - If not, it sends a
403 Forbiddenresponse.
- This new function takes an array of roles (e.g.,
- Fixed
/api/profile/:userId:- Inside the route, after authenticating and finding the
targetUser, we now have a crucialifstatement: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.
- Inside the route, after authenticating and finding the
- 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
authorizeRoleswill check ifreq.user.roleis ‘admin’. Only if both pass will the route handler execute.
- We added
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:
- Create a new route:
GET /api/order/:orderId - Make it vulnerable first: Allow any authenticated user to view any order by its
orderIdparameter. - Reproduce the IDOR vulnerability: Log in as Alice, then try to view Bob’s order (order ID 2).
- 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:
- 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.
- 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.
- 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.
- 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.
- 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. - 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
authenticateTokenmiddleware correctly populatingreq.userwith theidandrole? - Are your
ifconditions 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
- OWASP Top 10 (2021) - A01: Broken Access Control
- OWASP Cheat Sheet Series - Access Control Cheat Sheet
- MDN Web Docs - Authentication vs. Authorization
- Node.js Official Website
- Express.js Official Website
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.