The short version
Broken access control means your application lets users perform actions or access data they shouldn't be able to. It's CWE 284, and in the 2021 OWASP Top 10, it moved to the number one spot. Not because it's new, but because it showed up in 94% of applications tested.
It's the vulnerability AI coding tools are worst at preventing, because access control is about business logic, not syntax.
How it happens
The classic example is an IDOR (Insecure Direct Object Reference). Your app has an endpoint like:
GET /api/users/42/invoices
User 42 calls it and sees their invoices. But what if user 42 changes the URL to /api/users/43/invoices? If the server returns user 43's invoices, that's broken access control.
The server checked that the request came from an authenticated user. It didn't check whether that user should see those specific invoices.
Other common patterns:
- Accessing admin pages by navigating directly to
/adminwithout server side role checks - Modifying other users' data by changing an ID in a PUT/PATCH request
- Elevating privileges by editing the role field in a request body
- Accessing API endpoints that the UI doesn't expose but the server doesn't protect
The vibe coding problem
This is where AI generated code fails hard. When you ask an AI to build a dashboard, it generates beautiful React components with role based rendering:
// AI-generated "access control" - CLIENT SIDE ONLY
function AdminPanel({ user }) {
if (user.role !== 'admin') {
return <p>Access denied</p>;
}
return (
<div>
<h1>Admin Dashboard</h1>
<button onClick={() => fetch('/api/admin/users')}>Manage Users</button>
<button onClick={() => fetch('/api/admin/delete-all')}>Reset Database</button>
</div>
);
}
This looks like access control. It's not. It's UI decoration. The buttons are hidden, but the API endpoints are wide open. Anyone can open a terminal and run:
curl -X POST https://yourapp.com/api/admin/delete-all \
-H "Authorization: Bearer stolen-or-valid-token"
The UI check stopped the button from rendering. It didn't stop the request from executing.
This pattern appears in virtually every vibe coded app. The AI thinks about the UI. It doesn't think about what happens when someone bypasses the UI entirely.
The Lovable incident
This isn't theoretical. In early 2025, security researchers found that apps built with Lovable, a popular AI app builder, had wide open Supabase backends. The generated apps had no Row Level Security policies. Anyone who knew the Supabase URL could read and write directly to the database, bypassing the application entirely.
The UI looked secure. The database was public. That's broken access control at its most basic.
How to fix it
Rule #1: Every check must happen server side
Client side checks are for UX (don't show buttons users can't use). Server side checks are for security (reject requests users shouldn't make).
// Next.js API route with proper access control
export async function GET(request, { params }) {
const session = await getServerSession();
// Authentication: is the user logged in?
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
// Authorization: can THIS user access THIS resource?
const invoice = await db.invoice.findUnique({ where: { id: params.id } });
if (invoice.userId !== session.user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
return Response.json(invoice);
}
Every API route. Every server action. No exceptions.
Rule #2: Default to deny
If you forget to add an access check to an endpoint, the safest default is "deny." Use middleware that requires authentication on all routes, then explicitly mark public routes:
// middleware.js - protect everything by default
export function middleware(request) {
const publicPaths = ['/api/auth', '/api/public'];
const isPublic = publicPaths.some(p => request.nextUrl.pathname.startsWith(p));
if (!isPublic) {
const token = request.cookies.get('token');
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
}
}
Rule #3: Use database level controls
Supabase Row Level Security is one of the best tools for this. Instead of trusting your application code to check permissions, the database enforces them:
-- Only let users see their own invoices
CREATE POLICY "Users see own invoices"
ON invoices FOR SELECT
USING (auth.uid() = user_id);
-- Only admins can delete
CREATE POLICY "Admins can delete"
ON invoices FOR DELETE
USING (auth.jwt() ->> 'role' = 'admin');
Even if your application code has a bug, the database won't return data the user shouldn't see.
Rule #4: Test the negative cases
When you build a feature, test what happens when:
- A logged out user hits the endpoint
- A logged in user tries to access another user's data
- A regular user hits an admin endpoint
- Someone sends a request with a modified role or user ID
Automated tests for these cases catch regressions before they reach production.
Check your site
Want to know if your app has access control gaps? Scan it now and find out in 60 seconds.
Frequently Asked Questions
- What is broken access control in simple terms?
- Broken access control is when your app lets users do things they shouldn't be allowed to do. Viewing other users' data, editing someone else's profile, accessing admin features as a regular user, or deleting resources that don't belong to them.
- Why is broken access control ranked #1 on the OWASP Top 10?
- Because it's the most frequently found vulnerability in real-world applications. It appeared in 94% of applications tested by OWASP. It's easy to introduce and hard to detect with automated tools because the logic is specific to each application.
- Does using Supabase Row Level Security fix broken access control?
- RLS is excellent for database-level access control and prevents many common mistakes. But you still need to handle authorization logic for actions that go beyond simple CRUD, like business rules, feature flags, or multi-step workflows.
Your AI writes the code. We find what it missed.
Paste your URL. Security audit in 60 seconds.
Scan my app