ABDULKADERSAFI.COM
Back to Blog
Website Landing Page Typescript Web Application Nextjs website Audit

Next.js Just Patched 13 CVEs in One Release - Here's What Actually Broke

9 min read

13 CVEs across Next.js and React in one release — middleware bypass, SSRF, DoS, cache poisoning, XSS. Here's what to patch and what the bugs say about server components.

A terminal showing a Next.js security patch upgrade alongside a code editor with the React Flight protocol payload that triggered the denial-of-service CVE
Table of Contents

I read the May 2026 Next.js security release with my morning coffee and almost choked. Thirteen CVEs in one release. Six of them high severity. Middleware bypasses, SSRF, denial of service, cache poisoning, XSS — basically the full menu.

This is my third "Next.js CVE post" in the last twelve months. I'd say I'm tired of writing them, but honestly, the bugs themselves are interesting in a horrifying way. They're not careless one-off mistakes. They're symptoms of a framework that's pushing into harder and harder territory — custom protocols, server components, edge runtimes — and absorbing the cost of that complexity in production.

Let me walk you through the five that matter. Then we'll talk about what they actually mean.

Patch first, read second. Upgrade next to the patched release for your line (13.x, 14.x, or 15.x) before you do anything else. Everything below assumes you've already done that.

CVE #1: Middleware bypass via the i18n data route

Severity: 7.5/10. Affects the Pages Router with i18n enabled.

Here's the setup most people had. You enable i18n in next.config.js with two or more locales. You write a middleware file (in newer versions this got renamed proxy.ts specifically to discourage people from using it the way I'm about to describe). The middleware checks a session cookie before letting users hit /secret, and the matcher covers both the bare route and the localized variants — /en/secret, /fr/secret.

Inside /secret/index.tsx you have getServerSideProps returning an email, a feature flag, and a headline. The middleware is your only auth layer.

A user without a session cookie tries /secret → redirected to login. Middleware working. Cool.

Now watch this. You open devtools, find the __NEXT_DATA__ script, copy the build ID, and craft this URL:

/_next/data/{buildId}/secret.json

That endpoint serves the getServerSideProps payload directly. It returns the props. The email, the flag, the headline — all of it. The login page never happens.

Why? The matcher in the patched code correctly applies to the locale-prefixed data routes (/en/_next/data/..., /fr/_next/data/...) but missed the bare /_next/data/.../secret.json. The English and French versions were protected. The fallback was not.

I want to be honest about this one: middleware bypasses sound worse than they are in practice. If getServerSideProps had an actual server-side auth check inside it — if (!session) return { redirect: ... } — none of this would have leaked anything. Treating middleware as a hard authorization boundary was always a foot-gun. Next.js itself does not recommend it. But the framework letting you set up a matcher that silently misses a route is still a bug.

If you want a deeper dive on the right way to layer auth, I broke this down in Why Your Auth System Is Probably Wrong. The TL;DR: middleware is a filter, not a guard.

CVE #2: Denial of service via React Flight quadratic blowup

Severity: 7.5/10. Affects every Next.js app using server actions, plus any other framework on react-server-dom.

This is the one that genuinely impressed me as an attack. Beautiful, in the way that only stupid-simple algorithmic bugs can be.

When a server action fires, the client posts a payload in React Flight format — a custom serialization protocol React uses for component trees and form data. The first chunk has a header like $K1 which means: "look at all the form keys, find the ones that start with 1_, those are your real keys."

Easy enough. With a normal form, you have three or four keys total, the parser scans them once, you're done in microseconds.

Now imagine you craft a payload like this:

  • 200,000 junk keys (junk1, junk2, ... junk199999)
  • A header chunk that says: look for $K1, $K2, $K3, all the way to $K1000

The old deserialization code looped over every key for every K-reference. That is 200,000 × 1000 = 200 million string comparisons. The event loop blocks for several seconds while it grinds through them. Send a few of these in parallel and the server is dead.

I tested this on a stock Next.js app with a simple name/email/message server action. Normal request: 0.02 seconds. After firing the exploit once: about 6 seconds to respond. Chain it, and you've got yourself a one-laptop DoS.

The fix is cute. Instead of looping K * N times, the patch loads all keys into a list and walks them with a non-rewinding cursor. Once the cursor passes junk199999, it cannot go back. $K2 immediately hits the end and bails. Total comparisons: N + K, or about 201,000 in our worst case. From 200M to 201K with one data structure change.

This is also where I will quietly point out: TanStack Start does not use react-server-dom and was not affected. Vinxi-based forks and anything that copied the Next.js architecture inherited the bug. If you've been thinking about Next.js vs the other meta-frameworks, this is one of the data points.

CVE #3: Server-side request forgery (SSRF)

Severity: 8.6/10. Highest severity of the lot. Self-hosted only — Vercel deployments dodged this one.

This one made me audit every self-hosted deployment I have. Here's the shape.

You run Next.js on a VPS or in Kubernetes. Inside the same private network, you've got a Redis, a Postgres admin dashboard, an internal metrics endpoint — whatever. None of those services are exposed to the internet. Firewall locks them down. You feel safe.

An attacker sends a curl to your Next.js server with two specific properties:

  1. An Upgrade: websocket header
  2. A crafted request-target line pointing to an internal URL like http://localhost:6379/

Next.js parses the URL through its route resolver. The resolver sees a valid HTTP protocol and goes, "sure, I'll proxy that for you," and forwards the request to the internal service. The response — your Redis info, your admin dashboard, your internal API — comes back to the attacker.

The Next.js server became the lockpick into your private network. The firewall did not help because the firewall does not inspect requests from inside the box.

The patched code adds two new guards in resolveRoutes: a finished boolean and a statusCode. If the request is not actually a legitimate proxy target (per your rewrites, proxy, middleware config), finished stays false and the forwarding step is skipped. If somehow finished is true but a status code is already set, that's another red flag and the proxy logic is also skipped.

If you're self-hosting Next.js, this is the CVE you patch today. I've also pulled together a broader operational hardening list in Self-Hosted Security: The 17-Point Checklist That Stops You Getting Pwned — the SSRF is a perfect example of why the "Next.js is the only thing exposed" assumption is dangerous on its own.

CVE #4: Cache poisoning via the .rsc check

Severity: 5.4/10. Moderate, but very nasty in practice.

In Next.js, when you hit a route with a header that marks the request as wanting React Server Components data, the response is the RSC payload, not HTML. The cache layer is supposed to split these — one cache slot for the HTML version, a different slot for the RSC version. That keeps regular browser visits from getting back the binary RSC payload.

The check it used? "Does the URL path end with .rsc?"

That worked beautifully until someone realized URLs can have query strings. Send a request like:

GET /products?utm_source=email
Header: RSC: 1

The path technically ends with email, not .rsc. The check returns false. Next.js stores the RSC payload in the HTML cache slot. The next legitimate user clicks "Browse products" and gets back React Flight binary gibberish instead of the page.

Recreating it on a test deploy with a fake CDN: clear cache, send the poisoned curl, then visit normally in a browser — instant garbage response, with the cache happily serving it to everyone after.

The fix is one line: strip the query string before checking the suffix. The fact that it took until 2026 for someone to notice tells you something about how few people are auditing the framework's own caching logic. Most of us trust it implicitly, which is what makes a cache poisoning CVE in particular nasty — your "victim" is everyone downstream.

If you're deeply into the caching layer of Next.js (and you probably should be if you're shipping at scale), I went into the model in Next.js 15.5: A Developer's Guide. The mental model still holds — but the trust model just took a hit.

CVE #5: XSS in beforeInteractive scripts

Severity: 6.1/10. Affects anyone using <Script strategy="beforeInteractive"> with props derived from untrusted input.

Pattern most people don't realize they're vulnerable to:

<Script
  strategy="beforeInteractive"
  data-tracking-id={searchParams.tid}
/>

Looks harmless. data-* attribute, just a tracking ID. What's the worst that could happen?

Internally, Next.js renders this with an unsafe HTML injection path and JSON.stringify. The output looks like:

<script
  data-tracking-id="ABC123">
</script>

But JSON.stringify does not escape HTML characters. It escapes JSON special characters. So if the attacker crafts a URL where the tid value contains a closing script tag, a new opening script tag with arbitrary JS, and a residual fragment to swallow the rest — the rendered HTML breaks out of the original script tag, opens a new one, runs arbitrary JS, and hides the rest in a dead tag at the end so nothing visible leaks.

The victim sees a normal-looking page; the attacker has a code execution primitive in their browser context. From there: cookies, localStorage, session tokens, whatever Chrome's letting that origin do.

The fix is what you'd expect — HTML-escape the props before they hit the rendered output, not just JSON-escape them.

The general principle here is older than React: JSON.stringify is not a substitute for HTML escaping. They protect against different injection contexts. I see this confusion a lot in API code too — the patterns I broke down in Building Scalable APIs: REST Best Practices apply the same way: every untrusted input needs context-specific escaping for the place it ends up.

Were server components a mistake?

Let me be direct.

Two years ago, every project I started was Next.js. Server components felt like the future. Co-location of fetching and rendering, no client-server protocol I had to design myself, automatic streaming — it was a great pitch.

But look at the pattern in this release:

  • DoS via React Flight protocol — custom serialization
  • Cache poisoning via RSC detection — custom request classification
  • Middleware bypass via data routes — custom auth surface
  • SSRF via WebSocket upgrade path — custom proxy logic
  • XSS via beforeInteractive rendering — custom script injection path

Every single one of these is a bug in code Next.js had to write to support its own architecture. None of them existed in plain React rendering. The framework has been building new layers — RSC payload format, data routes, proxy semantics, hybrid script strategies — and each new layer is a new surface where the security model has to be re-derived from scratch.

I'm not saying server components are a mistake. I am saying the cost of running a framework that invents its own protocol is real, and Next.js is paying it. Every release at this point. Prime's tweet on this — "making your own protocol with serialization is incredibly hard" — is the polite version of what most of us are thinking.

For new projects I'm starting in 2026, I'm reaching for TanStack Start when I want React, Astro when I want a content site, and SvelteKit when I want something boring that just works. None of them are immune to bugs (every framework ships CVEs), but their attack surfaces are smaller because their architectural surface is smaller. There is just less custom code between my route handler and the response.

What to do now

Three things, in order:

1. Patch. Run npm outdated next react react-dom and bump to the patched versions for your line today, not tomorrow. The SSRF in particular is exploitable with a single curl, and the exploit fits in a tweet.

2. Audit your server-side props. If you have getServerSideProps (or RSC server fetches) that contain anything sensitive — emails, flags, internal IDs, tokens — add an explicit server-side auth check inside the function. Don't rely on middleware as the only gate. Middleware is a filter for most traffic, not a guarantee against directly-crafted requests.

3. Audit <Script> tags. Specifically beforeInteractive. If any of your props come from searchParams, params, cookies, or anything user-controlled — that's the XSS path. Either drop the strategy or sanitize the values before passing them in.

If you're self-hosting, also walk the network boundary. Could a successful SSRF reach a Redis, a metrics endpoint, an internal API? If yes, segment those services so they require some authentication even from inside the cluster. Trusting "internal-only" as a security boundary is what made the SSRF a real exploit instead of an inconvenience.

And if you're starting a new project this week — server components are not the only path anymore. They're one path. Pick the framework whose attack surface matches what you're actually shipping, not the one with the loudest marketing.

The bigger pattern is the same one I keep hammering on across every post: complexity is the bug. Every layer you add is a layer that has to be secured. Frameworks that hide complexity from you are still running it on your server. When it breaks, you find out together.

Patch the CVEs. Read the diffs. Then ask yourself if you actually need every feature your framework is selling you.

GET IN TOUCH

Let's Build Something Together

Whether you have a project idea, want to collaborate on a web or mobile app, or just say hello

Get in Touch
safi.abdulkader@gmail.com +965 60787763 Based in Kuwait & Lebanon