Skip to content

23 tools · 116 fixes · 32 issue guides · Sources: Supabase, GitHub, OWASP, Vercel + 32

Issues

Auth in middleware that skips API routes

A matcher that excludes /api (or only matches /dashboard) stops the login redirect on pages while Route Handlers and some Server Function POSTs stay open.

Share link · Share on X

By ShipSecurely Editorial · Updated · How we make these guides

criticalAccess controlAuth

Shows up in: Next.js, Vercel, Supabase

What it is

Protecting pages in middleware or proxy is not the same as protecting /api Route Handlers or Server Functions. Next.js documents that a Proxy matcher which excludes a path also skips Server Function calls on that path, and that authentication and authorization have to be verified inside each Server Function. A login redirect on /dashboard does not run for a request the matcher never selected.

Current Next.js docs call this file proxy.ts and the exported function proxy. The proxy reference says the middleware file convention is deprecated and has been renamed to proxy. Apps that have not migrated still use middleware.ts. The matcher behavior below is the same either way. The backend-for-frontend guide says third-party libraries may still refer to proxy as middleware. Supabase's Next.js server guide splits the filename by version: Next.js renamed the file in version 16. On Next.js 15 and earlier a proxy.ts file is never called, so the session never refreshes and users get signed out. Before that rename the file is middleware.ts and the function is middleware.

The proxy reference's negative-matching example skips API routes, static files, and metadata:

'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'

The comment on that example says the pattern matches every path except ones starting with api, _next/static, _next/image, and the named metadata files. Copying that snippet into an auth proxy is how an app "protects the app" and leaves /api/* with no proxy at all. A matcher that only lists page paths, such as ['/dashboard/:path*', '/app/:path*'], does the same thing to every route that is not on the list. The docs also say that without a matcher, proxy runs on every request, including static files, and that excluding _next/static and images keeps an auth redirect from blocking CSS and JavaScript. That is a reason to skip assets. It is not a reason to skip the API when the proxy is your only gate.

Server Functions are not their own routes. Next.js handles them as POST requests to the route where they are used. If the matcher excludes that page path, Proxy does not see the POST. The proxy reference says a matcher change, or a refactor that moves a Server Function to a different route, can silently remove Proxy coverage, and that you should always verify authentication and authorization inside each Server Function rather than relying on Proxy alone. The data security guide says a page-level authentication check does not extend to the Server Actions defined within it, and that you re-verify inside the action. The same guide's audit list calls out proxy.ts and route.ts as files with a lot of power.

The backend-for-frontend guide shows the other matcher shape when the proxy is meant to gate the API: matcher: '/api/:function*', returning 401 when the request is not authenticated. That guide's access section says to always verify credentials before granting access, and not to rely on proxy alone for authentication and authorization.

OWASP's authorization cheat sheet says permission has to be validated on every request, including one that did not come from your UI, and that a client-side check is not the decisive one. A matcher that never selects /api is a request the proxy does not validate.

This is a different bug from CVE-2025-29927. The Next.js advisory GHSA-f82v-jwr5-mffw describes an authorization bypass when the check lives in middleware, and the advisory page identifies it as CVE-2025-29927. The patched releases it lists are 12.3.5, 13.5.9, 14.2.25, and 15.2.3. For Next.js 11.x the advisory points at the workaround instead of a patched release. It says deployments hosted on Vercel are automatically protected, and that if you cannot upgrade yet you should stop external requests carrying the x-middleware-subrequest header from reaching the app. Upgrade a self-hosted app that is still on an affected version. After that upgrade, a matcher that never included /api is still open, because the proxy was never asked to run there.

How to check in 5 minutes

  1. Open middleware.ts or proxy.ts and read config.matcher. If it excludes api, or it only lists page paths such as /dashboard and /app, write down every Route Handler under app/api.

  2. For each app/api/**/route.ts, confirm the handler loads the session itself. If the only auth check is the middleware or proxy redirect, that handler is the bug.

  3. Signed out, on a staging app you own, copy a private API request from the network panel and send it again without cookies. A private read should come back 401 or 403, not the data.

  4. Search for "use server" exports. Each one should load the session inside the function. A matcher that skips the page where the function is used also skips that POST.

How to fix

Next.js

Check the session inside every Route Handler and Server Action that reads or writes user data. Middleware or proxy can be an extra filter. It is not the only control. The data security guide's pattern is to call your auth helper inside the action and stop before any write when there is no session.

If you keep a matcher, include the API paths that must be gated, or use a matcher that runs on those paths. The backend-for-frontend example uses matcher: '/api/:function*'. Excluding _next/static and _next/image is reasonable so auth redirects do not block CSS and images. Excluding api is only reasonable when those handlers authorize themselves.

export async function GET() {
  const session = await auth();
  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  // scope the query to session.user.id
}

Do not take userId from the query string and treat it as the caller. That is IDOR. A layout that redirects to login does not run for a Route Handler. See Auth checked only in the UI and Server Actions with no auth check.

Vercel

Vercel deployment protection can put a login in front of a preview URL. That is not production auth. A production deployment answers the public internet, and the handler still has to check the session. If you are self-hosting an older Next.js and the only check is middleware, read the CVE-2025-29927 advisory and upgrade. The advisory says deployments hosted on Vercel are already protected against that specific bypass.

Supabase

Inside the Route Handler or Server Action, create the Supabase client from that request's cookies so the query runs as the user and row level security applies. Supabase's Next.js server-side guide says SSR uses cookie-based session storage through @supabase/ssr, and that you create the server client per request because the cookies come from the request. Do not use a module-level secret-key client as the stand-in for a matcher that skipped /api. Supabase's API key guide says a secret key bypasses every row level security policy.

The same server guide says not to trust supabase.auth.getSession() in server code, including Proxy. It reads the session out of the cookie without revalidating it. Use getClaims(). That call verifies the token's signature every time. On projects with asymmetric signing keys, the default for new projects, it verifies locally against a cached copy of the project's public keys. On projects still using a symmetric secret, it calls the Auth server instead. The advanced SSR guide says this does not catch a revoked session: an unexpired token stays valid to getClaims(). Call getUser() where rejecting the session after logout matters.

The documented proxy refreshes the session cookie by calling getClaims(). That refresh does not run on a path the matcher skips, and refreshing a token is not the same job as rejecting a logged-out caller inside the handler the request actually hits.

Copy the fix prompt

Paste this into Cursor, Lovable, Bolt, or Claude Code. Read the diff before you merge it.

You are closing a middleware or proxy matcher gap. Do not add features.

1. Open middleware.ts or proxy.ts and read config.matcher. List every path the matcher does not cover, especially /api and any page path that uses a Server Function.
2. Open every app/api/**/route.ts file. If the handler reads or writes data and does not load the session itself, add that check and return 401 before the query. Ignore userId, role, and isAdmin from the request.
3. Open every "use server" export. Load the session inside the function. Do not treat a layout redirect or a matcher as the check.
4. If a matcher is the only gate for an API path, either include that path in the matcher or add the in-handler check. Keep the in-handler check either way.
5. If this app uses Supabase, create the user-scoped client from the request cookies inside the handler. Do not switch those calls to the secret key.

Do not add a login page, a new role system, or a referral link. List each handler and the check you added.

Questions

Is excluding /api in the matcher wrong by itself?

No, if every Route Handler under /api loads the session and refuses a missing one. The Next.js example excludes api so a site-wide proxy does not have to run on API routes. It is the bug when that proxy was the only place auth was checked. The handlers the matcher skips are ordinary HTTP endpoints.

Does a layout redirect protect the API?

No. A layout redirect decides which UI to render. Next.js says a page-level authentication check does not extend to Server Actions defined on that page, and a Route Handler is its own entry point. A logged-out client can call /api/... without ever rendering the layout.

Is this the same bug as CVE-2025-29927?

No. That advisory is a framework bypass: on affected self-hosted versions, a request can skip middleware. A matcher that excludes /api is your configuration, and it stays open after you upgrade. Patch the CVE if your version is in the affected range, and still authorize inside each handler. Next.js 11.x has no patched release on the advisory; the workaround is to stop external requests that carry x-middleware-subrequest.

Sources

Related

The weekly fix

The weekly fix: 3 real security mistakes in vibe-coded apps and how to fix them, every Tuesday.

One email, every Tuesday. A welcome note follows the first signup.

The security issues hitting Supabase, Vercel, Next.js, and Stripe apps

One short email a week, with the fix.

One email, every Tuesday. A welcome note follows the first signup.