3:47 AM, and a pager alert for unusual traffic on /api/admin/*. Routes that should have been sealed off behind middleware.ts, returning a clean 401 to anyone without a session cookie. Except the logs showed 200s. Hundreds of them, from IPs that had never touched the app before, all hitting the same handful of admin endpoints within a ten minute window.
That's roughly how a lot of security teams spent a night in early December 2025, when a campaign researchers later named Operation PCPcat started chewing through Next.js deployments. In under 48 hours it compromised more than 59,000 servers, stealing somewhere between 300,000 and 590,000 credential sets out of environment files, SSH keys, and cloud service tokens. A success rate over 64 percent. Each infected box scanning for new targets every 45 minutes, which is the kind of detail that makes you close your laptop and stare at the wall for a minute.
The vulnerability at the center of it, CVE-2025-29927, wasn't exotic. It came down to a single internal header, x-middleware-subrequest, that Next.js used to avoid infinite loops when middleware triggers its own rewrites. The framework trusted that header completely. It never checked whether the header actually came from Next.js itself or from a random curl command on the internet. Send the right value, and the runtime would skip your entire middleware chain, auth checks included, like they were never there.
If you're the kind of developer who put all your access control logic in middleware.ts because it felt clean and centralized (and, hand up, that used to be me), this is the part that stings.
Why Middleware-Only Auth Was Always A Bit Fragile
Middleware in Next.js runs at the edge, before a request ever reaches your route handler or page. That's exactly why it's tempting as an auth gate: one file, one place to check the session, and every downstream route inherits the protection. It looks like a security boundary. It behaves like a security boundary, most of the time.
But middleware is still application code, executing inside the same runtime as everything else, reachable by the same request that everything else sees. It isn't a firewall rule sitting outside your app's blast radius. It's a function that runs early. And any function that runs early can, in principle, be convinced not to run at all, whether through a header trust issue like this one, a misconfigured matcher pattern, or just a route added later that nobody remembered to protect.
The patched versions (12.3.5, 13.5.9, 14.2.25, 15.2.3, and later) fixed the header trust problem specifically. Good. Update your dependencies, obviously. But patching one bug in the gate doesn't change the fact that you built a single gate.
What Actually Held Up
The apps that shrugged this off weren't the ones with the fanciest middleware. They were the ones that treated middleware as a UX optimization (redirect unauthenticated users before they waste a full render) and kept the real authorization check inside the code path that actually touches sensitive data.
Something like this, in a route handler:
// app/api/admin/users/route.ts
import { getSession } from "@/lib/auth";
export async function GET(req: Request) {
const session = await getSession(req);
if (!session || session.role !== "admin") {
return new Response("Unauthorized", { status: 401 });
}
const users = await db.user.findMany();
return Response.json(users);
}
That check runs regardless of whether middleware executed, got bypassed, got skipped by a header nobody asked for, or simply wasn't matched because of a routing quirk. It's redundant with the middleware check, on purpose. Redundant is the point. Defense in depth is a cliche because it keeps being true, not because anyone enjoys writing the same if statement twice.
None of this is theoretical hand waving about "best practices," by the way. The teams that got hit hardest by PCPcat were, almost without exception, running the exact self-hosted, standalone-output configuration this CVE targets, with nothing checking authorization below the middleware layer. Once the header trick worked, the request landed directly on a route handler that assumed, wrongly, that it would never be reached by anyone who hadn't already cleared the gate upstream. The handler itself had no opinion about who was asking. That's the actual failure, not "someone forgot to patch," though that mattered too. The architecture had exactly one checkpoint, and the checkpoint had exactly one weakness.
Same idea applies one layer down, at the data access function itself:
export async function getUsersForAdmin(session: Session | null) {
if (session?.role !== "admin") {
throw new Error("Forbidden");
}
return db.user.findMany();
}
Now even if some new route handler forgets to check the session (it happens, someone's always in a hurry to ship a dashboard widget), the data layer still refuses to hand out rows it shouldn't. You end up with three places doing the "are you allowed to see this" check: middleware for the fast redirect, the handler for the request-level gate, and the query function for the last line of defense. Slower to write. Much harder to accidentally leave a hole in.
The Part Where I Admit The Old Way Was Convenient
I get why middleware-only auth spread. It's less code. Fewer places to remember to add a check when you're cranking out a new admin route at 6 PM on a Thursday because product wants it demoed Friday morning. Centralizing the logic in one file felt like good engineering, and honestly it read well in code review. "Look, one middleware function, protects everything under /admin." Nobody asks the follow-up question of what happens if that one function gets bypassed, because until December 2025 that wasn't really a live scenario most teams had internalized.
It is now. The attackers behind PCPcat weren't picking apart bespoke logic, they were running the exact same header trick against every vulnerable Next.js app they could find, automatically, at scale. If your only defense was "middleware checks the cookie," and middleware could be told to sit this one out, there was nothing behind it.
A Short List Of Things Worth Checking This Week
If you're running self-hosted Next.js with output: standalone (the Vercel-hosted deployments were not affected by this particular CVE, for what it's worth), confirm you're on a patched minor version. Then go look at whatever routes matter most, the ones touching PII, admin actions, billing, and ask whether the authorization check would still fire if middleware silently didn't run. If the honest answer is "no, it's middleware or nothing," that's worth an afternoon of refactoring before it's worth an incident review.
None of this is a knock on middleware as a tool. It's genuinely great for redirects, locale detection, feature flag routing, all the stuff that's annoying without it. It's just not a wall. It's a hallway monitor. Useful, but you still want a lock on the actual door.
There's also a quieter lesson buried in here about how we talk about architecture in code review. "Centralized" got treated as a synonym for "secure" for a long time, and those aren't the same property. A single checkpoint is easier to reason about, easier to test, easier to point to in a design doc. It is also, definitionally, a single point of failure. Sometimes the right tradeoff really is one central gate, especially for low-stakes routes. For anything touching money, PII, or admin capability at a payments company, the tradeoff isn't close. You want the boring, repetitive, slightly annoying version where three layers each independently refuse to hand over data to someone who shouldn't have it.
I patched our stuff, added the redundant checks, and went back to arguing about whether we needed another loading skeleton component. Turns out most weeks are still mostly that.
Sources:
- Operation PCPcat: Hunting a Next.js Credential Stealer That's Already Compromised 59K Servers, ITNEXT
- CVE-2025-29927: Next.js Middleware Authorization Bypass, OffSec
- Understanding CVE-2025-29927: The Next.js Middleware Authorization Bypass Vulnerability, Datadog Security Labs
- 59K Servers Hacked in 48 Hours: Inside Operation PCPcat, eSecurity Planet
United States
NORTH AMERICA
Related News
Disrupting a Criminal Scam Operation
22h ago

Greatness PhaaS Adds Device Code Phishing to Bypass MFA and Steal Tokens
4h ago

Apple just revealed a clue about its September iPhone event date
5h ago

Today’s Android app deals and freebies: Knight Bewitched, What Lies Underground, Northgard, more
5h ago
Take an extra $100 off your TechCrunch Disrupt 2026 pass: This week only!
5h ago