Runtime security

Were you affected by the Next.js middleware bypasses?

Yes if you ran unpatched Next.js and used middleware as your only authorization check. As of March 2025, CVE-2025-29927 is fixed in 12.3.5, 13.5.9, 14.2.25, and 15.2.3. Search logs for x-middleware-subrequest and put authorization plus Arcjet in the handler.

8 min read
In short: Yes if you ran unpatched Next.js and used middleware as your only authorization check. As of March 2025, CVE-2025-29927 is fixed in 12.3.5, 13.5.9, 14.2.25, and 15.2.3. Search logs for x-middleware-subrequest and put authorization plus Arcjet in the handler.

Were you affected by the Next.js middleware bypasses?

Yes if you ran an unpatched Next.js release and used middleware as your only authorization check. As of March 2025, CVE-2025-29927 is fixed in 12.3.5, 13.5.9, 14.2.25, and 15.2.3, and every 11.1.4+ release before those patches could skip middleware when the request carried a crafted x-middleware-subrequest header.

A second bug, CVE-2024-51479, let a pathname matcher miss the top-level route (/admin) while still protecting /admin/users. That one is fixed in 14.2.15. If authorization also ran inside the route or Server Component, a bypassed middleware was not a bypassed app.

Which Next.js versions were patched?

Use this table when you triage hosts. CVE-2025-29927 was disclosed on 21 March 2025 (GHSA-f82v-jwr5-mffw) and every patch shipped that month: 14.2.25 on 17 March, 15.2.3 on 18 March, 13.5.9 on 22 March, and 12.3.5 on 23 March.

Next.js lineFirst affectedFixed in
11.x and 12.x11.1.412.3.5 (no 11.x patch; upgrade)
13.x13.0.013.5.9
14.x14.0.014.2.25
15.x15.0.015.2.3

CVE-2024-51479 is a separate pathname bug. It affected 9.5.5 through 14.2.14 and is fixed in 14.2.15. A host on 14.2.15 is patched for that CVE and still needs 14.2.25 for CVE-2025-29927.

Patch first. Then search logs. The header-based bypass existed in code from 2022, so historical traffic can show exploit attempts from well before the March 2025 disclosure.

What is CVE-2025-29927?

CVE-2025-29927 (GHSA-f82v-jwr5-mffw) is a critical Next.js bug that lets an attacker skip middleware by sending a crafted x-middleware-subrequest header. Next.js uses that header internally to mark a request that already passed through middleware, so the runtime does not run the matcher again. If the header is accepted from the public internet, the "already checked" mark is attacker-controlled.

A blanket network rule that drops every x-middleware-subrequest header will stop the exploit and can also break legitimate internal hops. Cloudflare hit that when it tried a global block and had to roll it back. That is a generic WAF problem: the edge does not know which of your services is allowed to set the header. For more information about that gap, see does Next.js need a WAF?.

The durable fix is the patched Next.js release. Edge filters are a temporary mitigation while you upgrade.

What should you look for in logs?

The exploit signature is the header value, and it changed across Next.js lines. Search historical access logs, CDN logs, and application request history for the following:

Next.js line

Suspicious x-middleware-subrequest value

Before 12.2

pages/_middleware or pages/admin/_middleware on a sensitive path such as /admin

12.2 and later

middleware or src/middleware

15 and later

A repeated chain that fills MAX_RECURSION_DEPTH (5), such as middleware:middleware:middleware:middleware:middleware or the src/middleware:... equivalent

A match is not proof of data access. It is proof that someone tried the bypass, or that a client sent the internal header. Treat those rows as incident evidence: which paths, which times, which accounts if a session cookie was also present.

What is CVE-2024-51479?

CVE-2024-51479 (GHSA-7gfc-8cq8-jh5f) is a pathname matcher bug disclosed in December 2024. If middleware authorized by pathname.startsWith("/admin") and the matcher listed /admin/:path* plus /admin, Next.js could still allow the exact top-level path while denying /admin/users.

The vulnerable pattern looks like this:

import { NextResponse } from "next/server";
export function middleware(request: Request) {
const { pathname } = new URL(request.url);
if (pathname.startsWith("/admin") && !request.headers.get("authorization")) {
return new Response("Unauthorized", { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: ["/admin/:path*", "/admin"],
};

In logs, look for requests to the top-level protected path (/admin, /settings, /dashboard) that lack the header or cookie your middleware required, and that did not receive the 401 you expected. Sub-routes that were correctly denied do not clear the top-level miss.

What changed in Next.js 16?

Next.js 16 deprecated the middleware file convention and renamed it to proxy, to make the network boundary explicit. middleware.ts becomes proxy.ts, the named middleware export becomes proxy, and npx @next/codemod@latest upgrade does the rename for you.

Two details matter here. The proxy runtime is Node.js and cannot be configured, so the edge runtime is not supported there; if you need edge, stay on middleware for now. And the rename does not change the argument on this page. A layer that runs before routing is still the wrong place to hold your only authorization check, which is what CVE-2025-29927 demonstrated. Vercel's own guidance is to treat it as a last resort.

The rest of this page uses the middleware filename because that is what the affected versions shipped. On Next.js 16, read it as proxy.

Why is middleware not enough?

Both CVEs share a design lesson: middleware is a convenient first check, not the authorization system. It is a good place to redirect anonymous users into a login flow. It is a bad place to be the only function that decides whether /admin may run.

Authorization is an application-context question. The route, Server Component, or server action already knows the user and the object. Put the same check there. If middleware is skipped, the handler still denies. That is defense in depth, and it is why runtime application security lives in the handler rather than only at the edge.

How do you add defense in depth with Arcjet?

Patch Next.js, keep authorization in the route, and run a request-path filter in that same handler so a bypassed middleware still hits Shield, bot detection, and a rate limit. Middleware alone was bypassable. A handler that calls aj.protect(req) is not skipped by x-middleware-subrequest.

import arcjet, { detectBot, shield, slidingWindow } from "@arcjet/next";
import { auth } from "@/auth";
const aj = arcjet({
key: process.env.ARCJET_KEY!,
rules: [
shield({ mode: "LIVE" }),
detectBot({ mode: "LIVE", allow: [] }),
slidingWindow({ mode: "LIVE", interval: 60, max: 100 }),
],
});
export async function GET(req: Request) {
const decision = await aj.protect(req);
if (decision.isDenied()) {
const status = decision.reason.isRateLimit() ? 429 : 403;
return new Response("Forbidden", { status });
}
const session = await auth();
if (!session?.user || session.user.role !== "admin") {
return new Response("Unauthorized", { status: 401 });
}
// Load the admin view.
}

detectBot uses allow or deny, never a legacy block list. allow: [] denies every detected bot on this admin route. Shield looks for injection and other common attack shapes. The sliding window caps how often one identifier can hit the handler. None of these replace the auth() check. They sit in front of it so a scanner that skipped middleware still gets a deny.

You can also run Arcjet in middleware for coarse bot and Shield filtering. Do not make that the only layer. CVE-2025-29927 exists to remind you that middleware is skippable.

How do you use request history during incident response?

After you patch, you still need to answer three questions: were you affected, for how long, and what was accessed? Those answers drive customer notice and any legal disclosure.

If you have CDN or origin access logs, search them with the header values and top-level paths above. If you do not, you cannot reconstruct the incident from the application alone unless something else recorded requests.

Arcjet records request metadata (path, headers, decision) for every protect() call. The SDK drops the cookie header before anything is sent, and the request body stays in your process: only the sensitiveInfo rule reads it, and that analysis runs locally. Retention depends on your plan. That history is how you confirm whether a crafted x-middleware-subrequest ever reached a protected route, even when your platform logs were too coarse or already rotated.

Other headers, including authorization, are sent as-is, so treat the Arcjet dashboard as a system that sees request metadata and scope access to it accordingly.

For more information about putting those checks on every consequential handler, see how to detect and block attacks at runtime.

What should you do now?

Upgrade to the patched Next.js line in the table (as of March 2025: 12.3.5, 13.5.9, 14.2.25, or 15.2.3). Search logs for the x-middleware-subrequest values and for top-level paths that skipped a pathname matcher. Move authorization into the route. Add a handler-level runtime filter so the next middleware bug is not the only door. Middleware remains useful. It is not sufficient.

Frequently asked questions

Were you affected by the Next.js middleware bypasses?

Yes if you ran an unpatched release and used middleware as the only authorization check. As of March 2025, upgrade to 12.3.5, 13.5.9, 14.2.25, or 15.2.3, and search logs for a crafted x-middleware-subrequest header.

Which versions fix CVE-2025-29927?

12.3.5, 13.5.9, 14.2.25, and 15.2.3. The bug starts at 11.1.4. There is no 11.x patch; upgrade. CVE-2024-51479 is separate and is fixed in 14.2.15.

What do you look for in logs?

Requests whose x-middleware-subrequest value is pages/_middleware (pre-12.2), middleware or src/middleware (12.2+), or a five-deep middleware:middleware:... chain (15+). Also top-level paths like /admin that skipped a pathname matcher (CVE-2024-51479).

Is blocking the header at the WAF enough?

It can stop the exploit and can also break legitimate internal hops. Patch Next.js. Keep authorization in the route.

Why add Arcjet in the handler if middleware is patched?

Middleware alone was bypassable. aj.protect(req) in the route still runs when the matcher is skipped, and it is the place you already have the user and the object.

Application security in your code

Protect your application with Arcjet

Get rate limits, bot detection, and attack blocking in your request handlers.