OWASP A01 Broken Access Control Guide
Broken access control is the #1 OWASP risk. This guide explains IDOR, missing auth checks, JWT tampering, and how to fix them with real Next.js code examples.
Rod
Founder & Developer
Broken access control is the #1 vulnerability in the OWASP Top 10 2021 — and it's not close. OWASP found it in 34% of tested applications, up from #5 in 2017. It has 34 mapped CWEs (Common Weakness Enumerations), more than any other category. And yet it's one of the easiest classes of vulnerability to introduce accidentally — especially in modern JavaScript apps where auth logic gets scattered across middleware, components, and API routes.
This guide covers what broken access control actually looks like in code you write every day, how real breaches happened because of it, and what to do about it.
What Is Broken Access Control?
Authentication answers "who are you?" Authorization answers "what are you allowed to do?" Broken access control is what happens when authentication works fine but authorization doesn't.
The user logs in. Your app recognizes them. Then they navigate to /api/users/456 — and your app returns User 456's data even though they're User 123. Nobody bypassed your login page. Your auth token was valid. Your code just never checked whether the authenticated user was allowed to see that specific resource.
That gap is broken access control. OWASP's official A01:2021 entry describes it as the failure to enforce restrictions on what authenticated users can do. The key word is "enforce." You probably intended to restrict access. But intention without enforcement is just a comment in your code.
According to Veracode's State of Software Security 2025 report, access control failures remain one of the top findings in web application assessments — and they're increasingly common in AI-generated code that optimizes for working endpoints, not secure ones.
The broken access control encyclopedia entry on Data Hogo covers the vulnerability taxonomy in detail. This post focuses on the patterns you'll recognize from your own codebase.
The Five Patterns That Get You
1. IDOR — Insecure Direct Object Reference
This is the most common form. Your endpoint takes an ID from the URL or request body and returns the corresponding record — without checking ownership.
// BAD: Fetches any user's data — no ownership check
app.get('/api/orders/:orderId', async (req, res) => {
const order = await db.query(
'SELECT * FROM orders WHERE id = $1',
[req.params.orderId]
);
return res.json(order);
});User 123 is logged in. They change orderId from 100 to 101 in the URL. They now see User 456's order — including their address, payment method reference, and purchase history.
The IDOR pattern is documented as CWE-284 (Improper Access Control). The fix is simple: always join the ownership condition to your query.
// GOOD: Ownership check baked into the query
app.get('/api/orders/:orderId', async (req, res) => {
// req.user is populated by auth middleware
const order = await db.query(
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
[req.params.orderId, req.user.id]
);
if (!order) {
// Return 404, not 403 — don't confirm the resource exists
return res.status(404).json({ error: 'Not found' });
}
return res.json(order);
});Returning 404 instead of 403 is intentional. Returning 403 confirms that the resource exists — which is information you don't want to hand to someone probing your API.
2. Missing Function-Level Access Control
Some routes should only be callable by admins. If you don't explicitly check for that role, any authenticated user can call them.
// BAD: Admin endpoint with only authentication — no role check
export async function DELETE(req: Request) {
const { userId } = await supabase.auth.getUser();
if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
// Deletes any user — no check that the caller is an admin
await db.deleteUser(req.body.targetUserId);
return Response.json({ success: true });
}This endpoint checks that the caller is logged in. It doesn't check that the caller is an admin. Any authenticated user can delete any account.
// GOOD: Authentication + role check before destructive operation
export async function DELETE(req: Request) {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
// Fetch the caller's profile to verify their role
const { data: profile } = await supabase
.from('profiles')
.select('role')
.eq('id', user.id)
.single();
if (profile?.role !== 'admin') {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
await db.deleteUser(req.body.targetUserId);
return Response.json({ success: true });
}The role check happens server-side, against your database — not from a field in the request body or a JWT claim you haven't verified.
Endpoints without authentication is one of the most frequently flagged findings when we scan real repositories at Data Hogo. It shows up as both missing auth middleware and missing role checks on sensitive operations.
3. Auth Logic Only in the Frontend
This one is common in React and Next.js apps. The developer adds a check in the UI — "if user is not admin, don't show the delete button." The API route itself has no check.
// BAD: The button is hidden in the UI, but the API has no check
// Frontend hides the button — but anyone can call this endpoint directly
export async function POST(req: Request) {
const { targetUserId } = await req.json();
await db.banUser(targetUserId);
return Response.json({ success: true });
}The frontend check is worthless as a security control. Anyone with a browser's dev tools or curl can call your API directly. UI-only auth logic is documented as a specific failure pattern in auth logic in frontend only — and it's one of the checks Data Hogo runs on every Next.js repo scan.
Every access control decision must be enforced server-side. The frontend can hide elements for UX. It cannot prevent API calls.
4. JWT Tampering and Privilege Escalation
JWTs (JSON Web Tokens) are signed, not encrypted. The payload is readable by anyone with access to the token. If your app trusts claims from the JWT payload without verifying them server-side, you have a problem.
// BAD: Trusting the role claim from the JWT payload without verification
const decoded = jwt.decode(token); // decode, not verify!
if (decoded.role === 'admin') {
// Attacker can forge this claim if the signing key is weak or exposed
return adminData;
}The right approach is to verify the signature first, then look up the role from your database — not from the token payload.
// GOOD: Verify signature, then fetch role from the database
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.getUserById(decoded.sub);
// Trust the database, not the token claim
if (user.role !== 'admin') {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}Privilege escalation via JWT manipulation is a common finding in apps that use custom auth implementations rather than a well-tested library.
5. Path Traversal
Path traversal lets an attacker read files outside your intended directory by injecting ../ sequences into a file path parameter.
// BAD: Serving files from a user-supplied path with no sanitization
app.get('/files/:filename', (req, res) => {
const filePath = path.join('/var/app/uploads', req.params.filename);
res.sendFile(filePath);
});A request to /files/../../etc/passwd resolves to /etc/passwd. The path traversal pattern (CWE-22) can expose server configuration files, environment files, and private keys.
// GOOD: Resolve the path and verify it's still inside the uploads directory
app.get('/files/:filename', (req, res) => {
const uploadsDir = path.resolve('/var/app/uploads');
const filePath = path.resolve(uploadsDir, req.params.filename);
// Ensure the resolved path starts with the uploads directory
if (!filePath.startsWith(uploadsDir + path.sep)) {
return res.status(403).json({ error: 'Forbidden' });
}
res.sendFile(filePath);
});What a Real Breach Looks Like
Optus 2022 — Unauthenticated API Returns 9.8 Million Records
In September 2022, Australian telecom Optus suffered a breach that exposed the personal data of 9.8 million customers — including names, dates of birth, phone numbers, email addresses, and identity document numbers.
The root cause: an API endpoint that was reachable from the public internet and did not require authentication. The endpoint was a legacy system that had been exposed without a token requirement. An attacker discovered it, iterated through customer IDs sequentially, and extracted millions of records.
This is textbook broken access control — specifically, a missing authentication check on an endpoint that returned sensitive data. No sophisticated exploit. No zero-day. Just GET /api/customers/1, then GET /api/customers/2, then repeated 9.8 million times.
The breach resulted in a AUD $1.36 million fine from the Australian Communications and Media Authority and significant reputational damage. The root fix was trivially simple: require an authentication token.
Capital One 2019 — SSRF Combined with Overpermissioned IAM
The Capital One breach exposed data on approximately 106 million customers. The mechanism was a Server-Side Request Forgery (SSRF) vulnerability in a web application firewall configuration, which allowed an attacker to query the AWS EC2 instance metadata service at 169.254.169.254. That service returned temporary IAM credentials.
The access control failure: those IAM credentials were overpermissioned. The EC2 instance had an IAM role with far broader S3 access than it needed. An attacker who obtained the credentials could list and download files from S3 buckets they should never have been able to touch.
This is broken access control at the infrastructure layer — a server process with permissions it didn't need, combined with a vulnerability that let an attacker impersonate that process. The principle of least privilege is a form of access control, and it applies to your cloud resources as much as your API routes.
The Broken Access Control Prevention Checklist
These aren't abstract recommendations. They're the specific things to implement.
Deny by default. Every resource should be inaccessible unless your code explicitly grants access. Don't write code that allows access and then adds restrictions. Write code that denies access and then adds grants.
Server-side enforcement, always. Every access control decision happens on the server. Frontend checks are UX, not security.
Use middleware for authentication, use handlers for authorization. Your auth middleware verifies the session. Each handler verifies that the authenticated user has permission to perform the specific operation on the specific resource.
Add ownership to your queries. Don't fetch a record by ID and then check ownership in application code. Include the ownership condition in the database query itself — so an unowned record returns no rows rather than a row your code then has to decide about.
Implement RBAC properly. Roles belong in your database. Don't store roles in JWTs and trust them at face value. Look up the role fresh for sensitive operations.
Test access controls — explicitly. For every protected endpoint, write a test that logs in as User A and tries to access User B's resource. It should return 404. If it returns 200, you have an IDOR.
Log access control failures. A spike in 403 responses against a pattern of sequential IDs is an active IDOR probe. You want to know about it.
Audit open redirects. An open redirect can be used to bypass referrer-based access controls or to phish users with a trusted domain as the entry point.
How Data Hogo Detects Broken Access Control
When we scan repositories at Data Hogo, broken access control findings fall into several detectable patterns:
- Endpoints without auth middleware — routes that never call your auth verification function
- Frontend-only auth logic — conditional rendering or route guards without a corresponding server-side check
- Direct object references without ownership joins — database queries that use user-supplied IDs without a
WHERE user_id = $current_usercondition - CORS misconfiguration — overly permissive
Access-Control-Allow-Originheaders that enable cross-origin requests from untrusted domains - Admin routes without role checks — endpoints in
/admin/or with destructive operations that only check authentication
We scanned over 50 repositories while building Data Hogo's pattern library. Missing authentication checks were the single most common critical finding. IDOR patterns — identifiable from database queries that lack ownership conditions — were second.
Scan your repo free to see a prioritized list of access control findings in your codebase. The first scan is free, no credit card required.
Frequently Asked Questions
What is broken access control in web applications?
Broken access control happens when your app fails to enforce what authenticated users are actually allowed to do. A user can view another user's data, call an admin endpoint, or modify records they don't own — not because they bypassed login, but because your code never checked if they were allowed to do that specific thing. It's the #1 vulnerability in the OWASP Top 10 2021, found in 34% of tested applications.
What is an IDOR vulnerability?
IDOR (Insecure Direct Object Reference) is the most common type of broken access control. It happens when an endpoint uses a user-supplied identifier — like /api/orders/123 — to fetch a resource, but never checks whether the requesting user is the actual owner of that resource. Changing 123 to 124 in the URL shouldn't expose another user's order, but if there's no ownership check, it does.
How do I fix broken access control in a Next.js API route?
Always verify authentication first, then verify authorization. Check that the authenticated user actually owns the resource they're requesting — don't trust the ID in the URL or request body. Use middleware for authentication and explicit ownership checks inside each handler. Never rely on a resource being "hard to find" as a security control.
What real-world breaches were caused by broken access control?
Several major breaches trace back to broken access control. The 2022 Optus breach exposed data for 9.8 million customers because an API endpoint returned customer records without any authentication token required. The 2019 Capital One breach involved SSRF combined with overpermissioned IAM roles — a server-side form of broken access control. MOVEit (2023) had SQL injection that enabled full data access bypass across thousands of organizations.
Does automated scanning catch broken access control?
Partially. Static analysis tools like Data Hogo can detect missing authentication checks, endpoints without auth middleware, and auth logic placed only in the frontend. What they can't automatically verify is your business logic — whether User A is correctly prevented from accessing User B's specific data depends on your app's rules. Automated scanning catches the obvious patterns; manual review catches the edge cases.
Related Posts
OWASP A07 Authentication Failures Guide
OWASP A07 authentication failures with real code examples: weak passwords, JWT without expiry, localStorage tokens, no rate limiting, and how to fix each.
OWASP A02 Cryptographic Failures Guide
OWASP A02:2021 Cryptographic Failures is the #2 web vulnerability. Learn how plaintext passwords, weak hashing, and hardcoded keys expose your users — with real code examples.
OWASP A08 Data Integrity Failures Guide
OWASP A08:2021 covers CI/CD attacks, unsafe deserialization, and missing SRI. Learn how integrity failures happen and how to prevent them in your pipeline.