Broken Access Control: The #1 Web Vulnerability AI Tools Get Wrong
What is Broken Access Control?
Broken Access Control is when users can act outside their intended permissions. Think of it like a nightclub: the bouncer checks your ID (authentication), but your VIP pass determines which areas you can access (authorization). Broken Access Control is when anyone with an ID can walk into the VIP section.
According to OWASP's 2021 analysis, 94% of applications tested had some form of broken access control. It moved from #5 in 2017 to #1 in 2021, with over 318,000 CWE occurrences mapped to this category.
For vibe coders using AI tools, this is critical: AI-generated code reliably implements authentication but consistently misses authorization. The result? Your app knows WHO is logged in but doesn't verify WHAT they're allowed to do.
The Critical Distinction
- Authentication: Verifying who you are (login, password, token)
- Authorization: Verifying what you can do (permissions, roles, ownership)
AI tools excel at authentication. They fail at authorization.
Why AI Tools Get This Wrong
AI coding tools like Cursor, Claude Code, and Bolt are trained on tutorials and documentation that demonstrate authentication patterns extensively. Authorization? Not so much.
Here's what AI typically generates when you ask for "a user API":
// AI generates this - checks authentication only
app.get('/api/users/:id', requireAuth, async (req, res) => {
const user = await db.users.findById(req.params.id)
res.json(user) // Any authenticated user can access ANY user's data!
})What's missing? The authorization check:
// What you need - authorization check added
app.get('/api/users/:id', requireAuth, async (req, res) => {
// Authorization: Can this user access this resource?
if (req.user.id !== req.params.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' })
}
const user = await db.users.findById(req.params.id)
res.json(user)
})Why This Happens
- Training data bias: Most tutorials show authentication, not authorization
- Prompt ambiguity: "Create a user API" doesn't specify access rules
- Functional focus: AI optimizes for "does it work?" not "is it secure?"
- Context limitations: AI can't infer your business rules from code structure
According to Endor Labs research, 40-48% of AI-generated code contains security vulnerabilities, with access control issues being a leading category.
Common Attack Patterns
1. Vertical Privilege Escalation
A regular user performs admin-only actions. This happens when admin routes exist but don't verify the user's role.
// No role check - any authenticated user can delete anyone
app.delete('/api/users/:id', requireAuth, async (req, res) => {
await db.users.delete(req.params.id)
res.json({ success: true })
})2. Horizontal Privilege Escalation (IDOR)
User A accesses User B's data by changing an ID in the URL. This is the most common form of broken access control in vibe coded applications.
GET /api/orders/1001 ← Your order
GET /api/orders/1002 ← Someone else's order (attacker just changes the number)3. Missing Function-Level Access Control
Sensitive routes exist without any protection. AI sometimes generates admin routes accessible to anyone.
// Route exists but has no authentication or authorization
app.post('/api/admin/reset-database', async (req, res) => {
await db.reset() // Anyone can hit this endpoint!
res.json({ success: true })
})4. Metadata Manipulation
Attackers modify hidden fields or JWT claims to escalate privileges. Never trust role information from client input.
// Trusting client-sent role - attacker sends { role: 'admin' }
const { userId, role } = req.body
if (role === 'admin') {
// Grant admin access - but attacker controls this value!
}Code Patterns to Find
Search your vibe coded project for these vulnerable patterns:
Node.js/Express
// RED FLAG: requireAuth middleware but no ownership check
app.get('/api/resource/:id', requireAuth, handler)
// RED FLAG: User-supplied ID used directly
const item = await db.items.findById(req.params.id)
// RED FLAG: No filter by authenticated user
const orders = await db.orders.findMany() // Returns ALL ordersNext.js Server Actions
// RED FLAG: Server Action without authorization
export async function deleteUser(userId: string) {
// No check who's calling this!
await db.users.delete({ where: { id: userId }})
}Python/FastAPI
# RED FLAG: No ownership verification
@app.get("/orders/{order_id}")
async def get_order(order_id: int, user: User = Depends(get_current_user)):
return await Order.get(order_id) # Returns ANY orderfindById(req.params.id) or similar
without a userId filter, that's almost certainly an IDOR vulnerability.How to Fix
Principle 1: Deny by Default
Every route should deny access unless explicitly authorized. Never assume neutrality.
// Deny by default - explicit role check
app.delete('/api/users/:id', requireAuth, requireAdmin, async (req, res) => {
await db.users.delete(req.params.id)
res.json({ success: true })
})Principle 2: Verify Ownership on Every Request
Always filter database queries by the authenticated user's ID.
// SECURE: Always filter by user
app.get('/api/orders/:id', requireAuth, async (req, res) => {
const order = await db.orders.findFirst({
where: {
id: req.params.id,
userId: req.user.id // Ownership check - critical!
}
})
if (!order) {
return res.status(404).json({ error: 'Not found' })
}
res.json(order)
})Principle 3: Server-Side Validation Only
Never trust client-sent role or permission data. Always verify against server-side session.
// SECURE: Get role from server session, not request body
app.post('/api/admin/action', requireAuth, async (req, res) => {
// Role from session (server-controlled), NOT req.body
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' })
}
// Proceed with admin action
})Principle 4: Use Framework Security Features
Modern frameworks provide built-in access control. Use them instead of rolling your own.
- Supabase RLS: Row Level Security enforces access at the database level
- Next.js Middleware: Centralized route protection
- Express middleware: Reusable authorization checks
Framework-Specific Guidance
Next.js
Use Next.js middleware for route protection and always verify authorization in Server Actions.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const session = request.cookies.get('session')
// Protect admin routes
if (request.nextUrl.pathname.startsWith('/admin')) {
if (!session) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Additional role check needed in the actual route
}
return NextResponse.next()
}Supabase RLS
Row Level Security is
your best defense. Enable it on every table and use auth.uid() in policies.
-- Enable RLS on the orders table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Users can only see their own orders
CREATE POLICY "Users can view own orders" ON orders
FOR SELECT
USING (auth.uid() = user_id);
-- Users can only update their own orders
CREATE POLICY "Users can update own orders" ON orders
FOR UPDATE
USING (auth.uid() = user_id);Express Middleware Pattern
// Reusable authorization middleware
const requireOwnership = (resourceGetter) => async (req, res, next) => {
const resource = await resourceGetter(req.params.id)
if (!resource) {
return res.status(404).json({ error: 'Not found' })
}
if (resource.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' })
}
req.resource = resource
next()
}
// Usage
app.get('/api/orders/:id',
requireAuth,
requireOwnership((id) => db.orders.findById(id)),
(req, res) => res.json(req.resource)
)AI Fix Prompt: Audit for Broken Access Control
Copy this prompt into Cursor, Claude Code, or any AI assistant to audit your codebase:
Review my codebase for Broken Access Control vulnerabilities (OWASP A01:2021):
## Check 1: Authentication vs Authorization
For every protected route, verify:
- Is authentication required? (who are you?)
- Is authorization checked? (what can you do?)
- Flag routes with auth middleware but no authorization logic
Look for patterns like:
- requireAuth middleware followed by direct database access
- getServerSession() without ownership checks
- Depends(get_current_user) without permission verification
## Check 2: Direct Object References (IDOR)
Search for these vulnerable patterns:
- findById(req.params.id) without ownership filter
- findUnique({ where: { id } }) without userId check
- SELECT * WHERE id = $1 without AND user_id = $2
- Database queries using user-supplied IDs directly
For each one found, recommend adding:
- where: { userId: req.user.id } for Prisma
- .eq('user_id', user.id) for Supabase
- AND user_id = $user_id for raw SQL
## Check 3: Role-Based Access Control
For admin routes (/admin/*, /api/admin/*), verify:
- Role check exists (req.user.role === 'admin')
- Role is from server-side session, NOT client input
- Flag any role checks using req.body, req.query, or cookies
## Check 4: Server Actions (Next.js)
For each Server Action, verify:
- Uses getServerSession() or equivalent
- Checks user ownership before mutations
- Doesn't trust client-sent user IDs
## Check 5: Supabase/Database RLS
If using Supabase:
- List all tables with RLS disabled
- Check if policies use auth.uid()
- Flag any use of service_role key in client code
## Output Format
For each vulnerability found:
1. File path and line number
2. The vulnerable code pattern
3. Attack scenario (how an attacker exploits it)
4. Secure replacement code
5. Severity: Critical/High/Medium
Prioritize by severity and provide copy-paste fixes.Frequently Asked Questions
What is broken access control?
Broken Access Control happens when users can access data or perform actions beyond their permissions. It includes viewing other users' data (horizontal escalation), performing admin actions as a regular user (vertical escalation), or accessing unprotected functions. According to OWASP, 94% of applications tested had some form of broken access control, making it the #1 web vulnerability.
Why is broken access control OWASP #1?
Broken Access Control moved from #5 (2017) to #1 (2021) because it appears in nearly every application and is often critical when exploited. OWASP mapped over 318,000 CWE occurrences to this category. The rise of API-first development and AI-generated code has made it worse - AI tools reliably generate authentication but consistently miss authorization checks.
What's the difference between authentication and authorization?
Authentication verifies who you are (login, password, token). Authorization verifies what you can do (permissions, roles, ownership). AI tools excel at authentication (middleware, session checks) but fail at authorization because they can't infer your business rules. A bouncer checks your ID (authentication), but a VIP pass determines which areas you can access (authorization).
How do I prevent IDOR vulnerabilities?
IDOR (Insecure Direct Object Reference) is the most common form of broken access control. Prevent it by: 1) Always filtering database queries by the authenticated user's ID, 2) Using indirect references (UUIDs) instead of sequential IDs, 3) Implementing ownership checks before returning data, 4) Using framework features like Supabase RLS that enforce access at the database level.
How do AI tools cause access control issues?
AI tools like Cursor and Claude Code are trained on tutorials that demonstrate authentication patterns but rarely cover authorization. When you prompt "create a user API," the AI generates routes with login checks but no ownership verification. According to Endor Labs research, access control issues are among the top vulnerabilities in AI-generated code.
Related Security Topics
Scan Your Vibe Coded Project
vibeship scanner automatically detects broken access control patterns in AI-generated code, including IDOR, missing authorization, and unprotected admin routes.
Try vibeship scanner Free