Back to Blog
securityNext.jssupply-chainCVE

Recent security scares every Next.js and React team should have patched

A technical retrospective of the Next.js middleware bypass, cache-key confusion, the 2025 npm worms, and RSC server-side risks, plus the defenses that actually hold.

JohannaMarch 19, 20266 min read

Most of the incidents that hurt web teams over the last year and a half were not exotic. They were ordinary trust assumptions that turned out to be wrong: a header nobody thought was reachable, a cache key that collapsed two different responses into one, a build server that trusted whatever npm install handed it. What makes them worth revisiting is not the individual CVE numbers but the shape of the mistake. Fix the shape and you are covered against the next one too.

Here are four classes of bug that surfaced loudly in 2025, the mechanism behind each, how far the damage reaches, and what a durable fix looks like.

The Next.js middleware authorization bypass

In March 2025 the Next.js team disclosed CVE-2025-29927, an authorization bypass in the middleware layer. The mechanism is almost embarrassingly direct. Next.js uses an internal header, x-middleware-subrequest, to prevent middleware from recursing into itself when it makes internal subrequests. The runtime checked that header and, if it was present with the expected value, skipped middleware execution for that request. The problem: the header was not stripped from inbound traffic. An external client could set x-middleware-subrequest themselves and convince the framework that middleware had already run, so it ran the request straight through to the route handler.

If your app did authentication or role checks in middleware.ts — which the docs actively encouraged for gating /admin or /dashboard — an attacker could send the right header and walk past all of it. Blast radius depends entirely on what middleware was protecting. For a lot of teams that was the whole authenticated surface.

The immediate fix was to upgrade: patched releases landed across the 12, 13, 14, and 15 lines (14.2.25 and 15.2.3 among them). If you self-host and cannot patch instantly, stripping x-middleware-subrequest at the CDN or reverse proxy neutralizes it.

The deeper lesson is that middleware is a routing convenience, not a security boundary. It runs in a context designed for redirects and header rewriting, and it fails open by design — if it does not execute, the request still proceeds. Authorization belongs as close to the data as you can put it. Check the session again in the route handler, the server action, or the data-access layer. Middleware is fine as a first coarse filter and a place to issue redirects, but every sensitive read or write should re-verify the caller independently. Defense in depth is not a slogan here; it is the difference between one spoofable header and a compromise.

Cache poisoning and cache-key confusion

A cache is a machine that promises "same key, same response." Poisoning attacks break that promise by making the key omit something that actually changed the response body. The classic version is unkeyed input: a header, query parameter, or cookie influences what the origin returns, but the CDN or framework cache does not include it in the key. An attacker crafts a request that produces a malicious response, the cache stores it, and every subsequent visitor to that key is served the poisoned copy.

Next.js has had its own variants of this — issues where a crafted request could get an unexpected response cached, and issues around the Cache-Control behavior of Server Components payloads. But the class matters more than any single advisory. The failure modes to hunt for:

  • Response content that varies on a header the cache does not include in its key (and no matching Vary).
  • Redirects or error pages that get cached under a key that legitimate users will also request.
  • The React Server Components data payload (the ?_rsc= flavored responses) being cached differently from the HTML, so the two drift apart.

The fix is to make the cache key honest. If a value changes the response, it must be in the key or declared in Vary. Do not cache authenticated responses on shared infrastructure at all; scope anything user-specific to private. Normalize and validate inbound headers at the edge so unexpected ones cannot smuggle influence into the origin. And test it: send weird headers and duplicate query parameters at your caching layer in staging and watch what gets stored.

The npm supply-chain worms and dependency confusion

2025 was the year supply-chain attacks stopped being theoretical for JavaScript shops. Several waves stood out. A phishing campaign against maintainers of extremely popular low-level packages (the chalk/debug tier of dependency, downloaded billions of times weekly) pushed malicious versions that tried to hijack crypto transactions in the browser. Separately, a self-propagating worm — reported under the name Shai-Hulud — did something nastier: when it landed in a developer or CI environment, it scanned for credentials (npm tokens, GitHub tokens, cloud keys), used any npm token it found to publish trojanized versions of that maintainer's other packages, and exfiltrated the harvested secrets to public repositories. It replicated. One compromised token became dozens of compromised packages.

Two mechanisms are worth separating. The worm exploits implicit trust in install-time execution: postinstall scripts run arbitrary code on your machine and in CI with whatever ambient credentials are lying around. Dependency confusion is the adjacent trick — publish a public package with the same name as one of your private internal packages and a higher version number, and misconfigured tooling pulls the attacker's public copy instead of your private one.

The defenses are concrete and mostly boring, which is the point:

  • Commit lockfiles and install with npm ci, not npm install, so CI resolves exactly what you reviewed.
  • Disable install scripts by default in CI (npm config set ignore-scripts true) and allowlist the few packages that genuinely need them.
  • Pin your private scope to your internal registry and reserve those names publicly so nobody can squat them. Configure .npmrc so the internal scope never falls back to the public registry.
  • Give CI tokens the least privilege that works, scope them to single repositories, and prefer short-lived OIDC-issued credentials over long-lived tokens sitting in secrets. A token that cannot publish cannot be used to spread a worm.
  • Turn on registry provenance and require it where you can, so published artifacts are tied to a verifiable build.

The through-line: your dependency tree is code you did not write running with your permissions. Treat installs as an execution boundary.

Server-side request handling in RSC and server actions

React Server Components and server actions moved a lot of logic to the server, and with it a lot of server-side attack surface that used to live behind an API team's review. A server action is, mechanically, a POST endpoint that React wires up for you. It is callable directly. If you assumed it could only be invoked from your own form, you have an unauthenticated endpoint you did not know you exposed.

Two failure patterns dominate. The first is missing authorization inside the action — teams gate the page that renders the form but never re-check inside the action, so anyone who can craft the request can invoke it. The second is server-side request forgery: user input flows into a server-side fetch, and because that fetch originates from inside your VPC it can reach internal services, the cloud metadata endpoint, or admin ports that no external client could touch.

Fixes:

  • Authorize inside every server action and every RSC data fetch, not on the page that renders them. Verify the session and the specific object-level permission every time.
  • Validate and constrain any input that becomes part of a URL, hostname, or file path. For outbound fetches driven by user input, allowlist destinations and block link-local and private ranges — the metadata endpoint at 169.254.169.254 especially. On AWS, enforcing IMDSv2 blunts the worst outcome of an SSRF.
  • Treat the boundary between "component that renders" and "data the client is allowed to see" as explicit. Do not lean on a component not rendering to keep data secret; if it reached the server component, assume a determined caller can pull it.

The common thread

Every one of these came down to a single guard being trusted to hold: one header, one cache key, one install, one page-level check. The repair in each case is the same instinct — re-verify at the layer that actually touches the sensitive thing, and assume the layer above it can be skipped. Patch the specific CVEs, yes. But the teams that came through 2025 without a bad week were the ones who had already stopped betting the account on any single line of defense.