Next.js Middleware Bypass (CVE-2025-29927): Why Authorization Belongs in the Route, Not the Edge
In March 2025, a critical vulnerability was disclosed in Next.js: an attacker could bypass middleware by sending a single HTTP header. The flaw, tracked as CVE-2025-29927 and scored 9.1, affected applications that implemented authentication or authorization checks in Next.js middleware. The lesson is larger than the patch. Middleware is a routing and request-processing layer, not an authorization boundary, and treating it as one turns a convenience feature into a single point of failure.
1. What Next.js Middleware Does
Middleware runs before a request is completed. It can rewrite, redirect, add headers, or reject a request. A common pattern is:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
};
This looks like an access control gate. It is not. It is a request filter that runs in the same process as the application, and it can be skipped if the framework believes the request is an internal subrequest.
2. The Internal Subrequest Header
Next.js uses an internal header, x-middleware-subrequest, to prevent infinite recursion when middleware triggers a fetch that re-enters the application. The header value encodes the middleware path, repeated to a depth limit. In affected versions, an external request that supplied the expected value was treated as an internal subrequest and skipped middleware entirely.
# Illustration against an affected, authorized test instance
curl -i https://target.example.com/dashboard \
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'
The exact value depends on the Next.js version and project layout (for example, src/middleware when the file lives under src/). The depth limit is five. If the header matches, the middleware function does not execute, and the request proceeds directly to the route handler or page.
3. Affected Versions and Fixes
- Next.js 12: affected before 12.3.5; fixed in 12.3.5.
- Next.js 13: affected before 13.5.9; fixed in 13.5.9.
- Next.js 14: affected before 14.2.25; fixed in 14.2.25.
- Next.js 15: affected before 15.2.3; fixed in 15.2.3.
The advisory is GHSA-f82v-jwr5-mffw. The fix prevents external requests from supplying the internal header, so middleware cannot be skipped by a client. Upgrading is necessary but not sufficient if the application still relies on middleware as its only authorization control.
4. Why the Impact Is Severe
Many Next.js applications use middleware for exactly the checks that matter most:
- Redirecting unauthenticated users away from
/dashboard. - Blocking
/adminunless the session contains an admin role. - Enforcing tenant isolation by rewriting requests to a tenant-specific backend.
- Adding security headers or rejecting requests from blocked regions.
If middleware is skipped, the page or route handler still executes. A dashboard page that fetches data server-side may render the victim's data to an unauthenticated requester. An admin API route that assumed middleware had already checked the role may perform privileged actions. The vulnerability does not grant a session; it removes the check that was supposed to require one.
5. Testing Safely
Only test applications you own or are authorized to assess. A minimal verification sequence:
- Request a protected route without a session and confirm the expected redirect or 401/403.
- Repeat the request with the
x-middleware-subrequestheader and the version-appropriate value. - Compare status codes, response bodies, and
Locationheaders. - If the protected content appears, confirm that the response contains data the unauthenticated user should not see.
- Check whether the route handler independently verifies the session. If it does, the bypass may be limited to page rendering; if it does not, the impact is broader.
# Baseline
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://target.example.com/admin
# Probe (authorized testing only)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' \
https://target.example.com/admin
6. The Durable Fix: Authorize in the Data Layer
Middleware can remain useful for redirects, locale detection, and header manipulation. It should not be the only place where a security decision is made. Put the authorization check next to the data access:
// app/api/invoices/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { db } from '@/lib/db';
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
const session = await getSession(req);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const invoice = await db.invoice.findFirst({
where: { id: params.id, tenantId: session.tenantId }
});
if (!invoice) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(invoice);
}
This handler is safe even if middleware is bypassed, misconfigured, or removed. The session check and the tenant filter are in the same function that returns the data. That is the property to aim for: defense in depth, with the authoritative check at the point of use.
7. Additional Hardening
- Block the header at the edge. As a temporary mitigation for unpatched deployments, strip or reject
x-middleware-subrequestfrom all external requests at the reverse proxy or CDN. - Patch all Next.js instances. Include self-hosted deployments, preview environments, and internal apps. A staging instance on an old version is still an entry point.
- Inventory middleware usage. Search for
middleware.ts,middleware.js, andsrc/middleware. For each matcher, ask: what happens if this function never runs? - Add regression tests. Test protected routes without a session, with an expired session, with a different tenant, and with the internal header present.
- Log authorization decisions. Record the authenticated subject, the resource, and the decision. A sudden cluster of 200 responses on protected routes without a session is a detection opportunity.
8. Conclusion
CVE-2025-29927 is a reminder that frameworks optimize for developer experience, not for security invariants. Middleware is an elegant place to express routing rules, but it is a fragile place to enforce authorization. The header that caused the bypass was an internal implementation detail; the real vulnerability was the assumption that a request filter could not be skipped. Patch the framework, block the header, and move the authoritative check to the function that actually reads or mutates the protected resource. Then a bypassed middleware layer becomes a missed redirect, not a data breach.