Next.js Middleware Is Not Enough to Protect Your Routes: The Auth Bypass Your AI Set Up (With Code)

July 21, 2026 · 2046 words

Next.js middleware authentication feels like a solved problem when your AI writes it. You ask Cursor or Claude Code to "protect my dashboard", it drops a middleware.ts file that checks for a login cookie and redirects everyone else to /login, the page stops loading when you're logged out, and you move on. It looks locked. It isn't. Next.js middleware is not enough to protect your routes — and if it's the only place your AI put the auth check, a stranger can walk straight past it and read your users' data.

This isn't a hypothetical. In March 2025, a real vulnerability — CVE-2025-29927, rated 9.1 out of 10 — showed that attackers could bypass Next.js middleware entirely by adding one HTTP header to their request. Every "protect the route in middleware" app was exposed until it was patched. But here's the part that matters even after the patch: the official Next.js docs have always said middleware should not be your only line of defense. This guide shows the exact insecure pattern AI assistants generate, why middleware alone fails, and the fix — a Data Access Layer — that you can paste in today.

What is middleware, in plain English?

Middleware is a file (middleware.ts) that Next.js runs before a request reaches your page. Think of it as a bouncer standing at the front door of your app. Every request walks up, the bouncer peeks at it, and decides: let you in, or send you to the login page.

The problem is that a bouncer at the front door doesn't guard the back door, the windows, or the fire escape. In a Next.js app, your data has many entrances — pages, Server Actions, Route Handlers, React Server Component requests — and middleware only reliably stands at one of them. Worse, as CVE-2025-29927 proved, the bouncer can be told "I'm staff, let me through" and he'll step aside.

Diagram comparing insecure Next.js middleware-only authentication where an attacker bypasses the check versus a secure Data Access Layer that protects the database

The official Next.js authentication docs put it plainly: middleware checks should be optimistic only — quick, cookie-based filters for redirecting obvious guests — and "it should not be your only line of defense in protecting your data. The majority of security checks should be performed as close as possible to your data source."

The code your AI wrote

Ask an AI assistant to secure your app and you'll almost always get something like this — auth logic living only in middleware:

// middleware.ts — the ONLY auth check in the app (insecure)
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

const protectedRoutes = ['/dashboard', '/admin']

export async function middleware(req: NextRequest) {
  const path = req.nextUrl.pathname
  const isProtected = protectedRoutes.some((r) => path.startsWith(r))

  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  // If no valid session, bounce to login
  if (isProtected && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
// app/dashboard/page.tsx — trusts that middleware already checked (insecure)
export default async function DashboardPage() {
  // No auth check here — "middleware handles it"
  const secrets = await db.query.secrets.findMany()
  return <SecretList items={secrets} />
}

It runs. It even feels safe: log out, visit /dashboard, and you get redirected. Ship it, right?

Why it's dangerous

There are three separate ways this pattern leaks your data, and a vibe coder will not spot any of them by clicking around the app.

1. The middleware can be skipped entirely. CVE-2025-29927 abused an internal header called x-middleware-subrequest. Next.js used this header behind the scenes to avoid running middleware twice on itself. The bug: it never checked that the header came from inside Next.js. So an attacker could send a normal request with that header set, and Next.js would think "middleware already ran" and skip it completely — walking straight into /admin. Vercel's own postmortem confirms the mechanism and notes it affected self-hosted apps using next start or output: 'standalone'. It's patched now (in 12.3.5, 13.5.9, 14.2.25, and 15.2.3), but it's a permanent lesson: a single choke point is a single point of failure.

2. Pages render without their layout. Even with no CVE at all, Next.js can serve a page's content without running the parent layout — and the same idea applies to any auth check that isn't attached to the data itself. A maintainer spelled this out in an official Next.js discussion: "adding an auth check to a layout is not enough to protect its contents... If you are trying to protect page contents by adding auth to a layout, this is a security vulnerability and you should patch it as quickly as you can." You can reproduce it by adding an RSC=1 header to a request and watching protected content come back with no auth check.

3. Middleware can't safely reach your database. Middleware runs on every single request, including prefetches, so the docs warn you to keep it to cheap cookie reads and avoid database lookups. That means middleware can confirm a cookie exists, but not that the user actually owns the specific record they're requesting. A logged-in user can still read another user's data — a flaw called IDOR (Insecure Direct Object Reference) — because the only "guard" never looked at the database.

Put simply: a cookie is not a permission slip, and the front door is not the vault.

Three ways Next.js middleware-only authentication leaks data: middleware bypass via CVE-2025-29927, layout-only optimistic checks, and missing database ownership checks (IDOR), each with its fix

RiskWhat an attacker can doThe fix
Middleware bypass (CVE-2025-29927)Add one header, skip auth, open /adminPatch Next.js and check auth at the data layer
Layout/optimistic-only checksFetch page data with an RSC=1 header, no loginVerify the session inside the page/component
No DB check in middlewareRead another user's records (IDOR)Verify ownership where you query the database
Cookie exists but is forged/expiredSlip past a shallow "cookie present?" testDecrypt and validate the session server-side

The fix: a Data Access Layer (DAL)

The Next.js team's recommended fix is to stop treating middleware as your lock and instead put the real check next to your data. You do this with a Data Access Layer — a small server-only file that every database call goes through, so the auth check can never be forgotten.

Step 1 — Build the DAL. This file verifies the session on the server, close to the data. The server-only import guarantees this code can never accidentally ship to the browser, and React's cache avoids re-checking on every call in one render.

// app/lib/dal.ts — the fix
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect('/login')
  }

  return { isAuth: true, userId: session.userId }
})

Step 2 — Query through the DAL, and check ownership. Every data read verifies the session first, then only returns rows that belong to the current user. This is what closes the IDOR hole middleware can't.

// app/lib/data.ts — the fix
import 'server-only'
import { verifySession } from '@/app/lib/dal'
import { db } from '@/app/lib/db'

export async function getMySecrets() {
  const session = await verifySession() // auth check lives WITH the data

  // Only rows owned by THIS user — ownership check, not just "logged in?"
  return db.query.secrets.findMany({
    where: (s, { eq }) => eq(s.userId, session.userId),
  })
}

Step 3 — Use it in the page. The page no longer trusts middleware. The check travels with the query.

// app/dashboard/page.tsx — the fix
import { getMySecrets } from '@/app/lib/data'

export default async function DashboardPage() {
  const secrets = await getMySecrets() // verifies session + ownership
  return <SecretList items={secrets} />
}

Step 4 — Keep middleware, but demote it. Middleware is still useful for the fast, friendly redirect (sending logged-out visitors to /login before a page even renders). Just treat it as a convenience layer, never the security layer. And guard the specific mutations too — the docs are explicit that Server Actions and Route Handlers must "be treated with the same security considerations as public-facing API endpoints," so each one calls verifySession() on its own. If your login system itself was AI-generated, harden it with our secure authentication guide, and if your data lives in Supabase, layer database-level rules on top with our Supabase RLS guide.

How to test the fix

You don't need special tools. Two checks catch most of this.

Check 1 — Are you patched against the bypass? Confirm your Next.js version is current:

npm ls next
# Must be >= 12.3.5, 13.5.9, 14.2.25, or 15.2.3 (newer is better)

Check 2 — Does your data leak without a login? Log out (or use a fresh private window with no cookies) and hit a protected page directly, simulating the "skip the middleware" trick with a raw request:

# No session cookie. If protected data comes back in the HTML, you're exposed.
curl -s https://your-app.com/dashboard \
  -H 'RSC: 1' | grep -i "secret"

If that returns your private content, your auth still lives only at the front door. With the DAL in place, the request should redirect or return nothing sensitive, because the page can't fetch data without a valid session.

Ask your AI to fix it

Paste this into Cursor, Claude Code, or Copilot:

"Right now auth is only checked in middleware.ts. Refactor so security does not depend on middleware. Create a server-only Data Access Layer (app/lib/dal.ts) with a cached verifySession() that decrypts the session cookie and redirects to /login if there's no valid userId. Route every database read through functions that call verifySession() first and filter results by the current user's ID (ownership check to prevent IDOR). Add a verifySession() check inside every Server Action and Route Handler too. Keep middleware only for the optimistic logged-out redirect. Show me the diff."

Your Next.js middleware security checklist

Next.js middleware security checklist covering patched versions, a server-only Data Access Layer, per-query ownership checks, Server Action verification, and the logged-out curl test

  • Next.js is patched — version ≥ 12.3.5 / 13.5.9 / 14.2.25 / 15.2.3 (npm ls next).
  • Auth check lives with the data, in a server-only Data Access Layer — not only in middleware or a layout.
  • Every DB query filters by the current user's ID (ownership check), not just "is someone logged in?".
  • Each Server Action and Route Handler calls verifySession() itself.
  • The session cookie is decrypted and validated, not just checked for existence.
  • Middleware is treated as convenience, not the security boundary.
  • You ran the logged-out curl test and no private data came back.

If you'd like a second pair of eyes on your actual code, I run hands-on security audits of vibe-coded Next.js apps — I trace every route, Server Action, and database query, run the logged-out bypass tests above, and hand you a prioritized fix list. Email me at [email protected] with your repo or stack, and I'll tell you where an attacker would slip past your middleware first.

Key Takeaway

Middleware is a bouncer at the front door, not a lock on the vault. AI assistants love to put your only auth check there because it looks protected in the browser — but pages, Server Actions, and RSC requests are separate entrances, and CVE-2025-29927 proved the front door itself can be skipped. Move the real check next to your data with a Data Access Layer that verifies the session and checks ownership on every query. Keep middleware for the friendly redirect, and let the vault guard itself.

For the bigger picture on shipping AI-generated code safely, start with our pillar guide on vibe coding security.