Am I Hackable?
Back to Learn

Broken Access Control: OWASP #1 and AI's Biggest Blind Spot

Benji··5 min read

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:

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:

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