• Agents
  • Docs
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Blog
  • Docs
  • FAQ
  • Glossary

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Back to Blog
Security

Admin Panels Wide Open: The Door AI Forgot to Lock

Your AI built a beautiful admin dashboard. It also made it accessible to anyone who types /admin. Here's how to find exposed admin routes and lock them down in minutes.

Flowpatrol TeamApr 5, 20269 min read
Admin Panels Wide Open: The Door AI Forgot to Lock

Try this right now

Open an incognito window. Type your app's URL followed by /admin. No cookies. No session. No login.

If you see a dashboard — users, settings, data exports, anything — that page is live for everyone. Not just you. Anyone who guesses the URL. Any bot crawling common paths. Any curious person who opens DevTools, reads your client-side routes, and types one of them in.

Here's the thing: your admin panel probably works perfectly. The data loads. The buttons respond. The layout looks polished. The AI built exactly what you asked for. It just didn't add a lock.

This is one of the most common patterns in apps built with Lovable, Bolt, Cursor, and v0. The admin panel exists. It functions. And it's open to the internet.


The code that ships

When you prompt "add an admin dashboard," here's what AI typically generates:

// app/admin/page.tsx
import { getUsers } from "@/lib/db";

export default async function AdminPage() {
  const users = await getUsers();

  return (
    <div>
      <h1>Admin Dashboard</h1>
      <p>Total users: {users.length}</p>
      <table>
        {users.map((user) => (
          <tr key={user.id}>
            <td>{user.email}</td>
            <td>{user.role}</td>
            <td>{user.created_at}</td>
          </tr>
        ))}
      </table>
    </div>
  );
}

Read it carefully. There's no getSession() call. No role check. No redirect. The page fetches every user from the database and renders them. That's all it does. That's all you asked it to do.

The AI also generates API routes to power the dashboard:

// app/api/admin/users/route.ts
import { NextResponse } from "next/server";
import { getUsers } from "@/lib/db";

export async function GET() {
  const users = await getUsers();
  return NextResponse.json(users);
}

Same story. Hit /api/admin/users with a curl command and you get every user in the database as JSON. Emails, roles, creation dates. No auth required.

An admin dashboard page accessible via a direct URL with no login gate between the visitor and the data


The backdoor that looks like a lock

The open route is the obvious problem. There's a subtler one that's arguably worse: the route that looks protected but isn't.

AI models have seen enough codebases to know that admin routes should have auth. So sometimes they generate a check. The check just doesn't work.

// app/api/admin/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getUsers } from "@/lib/db";

export async function GET(req: NextRequest) {
  const isDebug = req.headers.get("x-debug") === "true";
  const adminKey = req.headers.get("x-admin-key");

  if (isDebug || adminKey === "letmein") {
    const users = await getUsers();
    return NextResponse.json(users);
  }

  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

This returns 401 for normal requests. It feels protected. You test it in the browser, get an error, and move on.

But anyone who sends a single header gets full access:

# Looks protected...
curl https://yourapp.com/api/admin/users
# {"error": "Unauthorized"}

# One header changes everything
curl -H "x-debug: true" https://yourapp.com/api/admin/users
# [{"id": 1, "email": "alice@example.com", "role": "admin"}, ...]

These debug bypasses — x-debug, x-admin-key, x-internal, hardcoded tokens like letmein or admin123 — are placeholder auth that AI generates during development. They ship to production because they return 401 in casual testing. The security is a prop on a stage set. It only works if nobody walks behind it.


Why AI builds it this way

This isn't a bug in a specific tool. It's a structural pattern in how all AI code generation works.

AI builds features, not systems. "Build an admin panel" is a feature request. The AI delivers the UI, the data fetching, the layout. Authentication is a separate system. Authorization is another. The AI treats each prompt as self-contained. It doesn't ask: "How does this page fit into the security model of the whole app?"

Auth makes the page harder to test. If the AI adds an auth gate, the page won't render during development unless there's an active admin session. The AI doesn't have a session. It can't verify its own output through a login wall. So it skips the wall to make the page work.

The absence of security is invisible. Your admin panel functions identically with and without access controls. Data loads. Buttons click. Everything renders. You only notice the missing lock when someone who shouldn't be there walks through the door.

Two versions of the same admin panel — one with an auth check redirecting unauthorized users, one serving data to anyone

Frameworks default to open. Next.js doesn't protect routes by default. Express doesn't require auth on endpoints. The framework assumes you'll add security. The AI assumes the framework handles it. The result: nobody handles it.


What's behind that door

An exposed admin panel isn't a minor data leak. It's usually the highest-value target in the entire application. Here's what we see behind unprotected admin routes:

User tables. Full lists of emails, roles, account creation dates. Sometimes password hashes. Sometimes plaintext passwords. The ability to create, delete, or modify accounts — including promoting yourself to admin.

Data exports. CSV or JSON download of your entire database. Customer records, transaction history, uploaded files. One endpoint, one click, everything.

Application config. Feature flags, third-party API keys, payment gateway credentials, environment variables rendered right on the settings page. An attacker doesn't need to find your .env file — your admin panel shows it.

Database explorers. Some AI-generated admin panels include a query runner — a text input where you type SQL and execute it against production. This escalates from data leak to full system takeover. DROP TABLE users; is a valid query.

Logs. Application logs that reveal internal endpoints, stack traces, user activity patterns, and error messages containing sensitive data. A map of everything else worth attacking.

The admin panel is often the single page where everything in your app comes together. If it's unprotected, everything is unprotected.


How to check your app in five minutes

Do this before you read the fix section.

1. Hit admin routes from an incognito window

Open a private browser (no cookies, no session) and try each of these on your app:

  • /admin
  • /dashboard/admin
  • /dashboard
  • /api/admin
  • /api/admin/users
  • /api/admin/settings
  • /_admin
  • /internal

If any URL returns data or renders a page instead of redirecting to login, it's exposed.

2. Search for debug headers in your codebase

grep -r "x-debug\|x-admin\|x-internal\|x-bypass\|letmein\|admin123\|secret" \
  --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" \
  -l

Every match needs review. If a header value grants access to a route, remove it.

3. Audit every API route

List every file in your API directory (app/api/ in Next.js). For each one, ask: what happens if an unauthenticated user calls this endpoint? If the answer is "they get data back," that endpoint is open.

A checklist showing admin route testing steps — incognito visit, header search, API audit


The fix: middleware, not page-level checks

The principle is simple: protect admin routes at the middleware level, not inside each page. Individual pages forget. Middleware can't be accidentally skipped.

Next.js middleware

This runs before every matching request. No session? Redirected to login. Not an admin? Sent home. The admin page never renders.

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getSession } from "@/lib/auth";

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  if (pathname.startsWith("/admin") || pathname.startsWith("/api/admin")) {
    const session = await getSession(request);

    if (!session) {
      return NextResponse.redirect(new URL("/login", request.url));
    }

    if (session.user.role !== "admin") {
      return NextResponse.redirect(new URL("/", request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/admin/:path*", "/api/admin/:path*"],
};

The matcher config means this code only runs on admin paths. The two checks are explicit: first authentication (is anyone logged in?), then authorization (are they an admin?). Both must pass.

Route group layouts

In Next.js, you can also enforce auth at the layout level using route groups. Every page inside the group inherits the check:

// app/(admin)/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getSession();

  if (!session) {
    redirect("/login");
  }

  if (session.user.role !== "admin") {
    redirect("/");
  }

  return <div>{children}</div>;
}

This is defense in depth. Use middleware for the hard gate. Use the layout as a second layer. You can't accidentally add an unprotected admin page inside this group.

Express / Node.js

Same concept, different syntax. One middleware function protects every route on the router:

// middleware/requireAdmin.ts
import { Request, Response, NextFunction } from "express";

export function requireAdmin(req: Request, res: Response, next: NextFunction) {
  if (!req.session?.user) {
    return res.status(401).json({ error: "Not authenticated" });
  }

  if (req.session.user.role !== "admin") {
    return res.status(403).json({ error: "Not authorized" });
  }

  next();
}

// routes/admin.ts
import { Router } from "express";
import { requireAdmin } from "../middleware/requireAdmin";

const router = Router();
router.use(requireAdmin);

router.get("/users", async (req, res) => {
  const users = await getUsers();
  res.json(users);
});

Every route on this router is protected by requireAdmin. New endpoints inherit the guard automatically.

The rule

If you have to remember to add an auth check to each new admin page, you will forget on the one that matters most. Put the check in one place — middleware, a layout wrapper, a router guard — and make every admin route inherit it.


What you should do right now

You built something real. The admin panel is the last door to lock before it's production-ready.

  1. Test your admin routes in incognito. No cookies, no session. If you see data, it's exposed. Two minutes.

  2. Grep for debug headers. Search for x-debug, x-admin, letmein, and similar strings. Remove every backdoor bypass you find.

  3. Move auth to middleware. Don't check auth inside each page. Use framework-level middleware or layout guards that protect the entire /admin tree at once.

  4. Check roles, not just sessions. Being logged in isn't the same as being an admin. A regular user who navigates to /admin should see a redirect, not a dashboard.

  5. Scan with Flowpatrol. Flowpatrol checks for unprotected admin routes, exposed API endpoints, and debug header bypasses automatically. Paste your URL, get a report, and know exactly where you stand before someone else finds out.

Back to all posts

More in Security

Your Sign-Up Flow Has a Backdoor
Apr 4, 2026

Your Sign-Up Flow Has a Backdoor

Read more
Cross-Site Scripting in React: Why dangerouslySetInnerHTML Actually Is Dangerous
Apr 2, 2026

Cross-Site Scripting in React: Why dangerouslySetInnerHTML Actually Is Dangerous

Read more
IDOR: The Vulnerability AI Can't See
Mar 29, 2026

IDOR: The Vulnerability AI Can't See

Read more