Learn / Attack archive / Lesson 22

When the framework is the hole — React2Shell and the Next.js bypass

Two recent flaws in the most popular web stack, one that skipped every login check and one that ran attackers' code, and why checking access in one place only is fragile.

Last updated: 2026-09-23

What it is

Sometimes your code is fine and the framework under it has the bug. Because one framework runs millions of sites, a single flaw becomes a flaw in all of them on the same day, and attackers scan the whole internet for it within hours.

Two recent cases show the two classic shapes:

Why it is a rule

Next.js middleware bypass — CVE-2025-29927, March 2025

Next.js lets you put login checks in middleware, a function that runs before every page. To avoid running middleware twice on internal sub-requests, Next.js marked those with a header called x-middleware-subrequest. The flaw: it believed that header when it came from the outside world too.

An attacker who added that header to their request made Next.js think the middleware had already run, so it skipped it entirely. Every login check, redirect and security header in middleware vanished. Any self-hosted app that protected pages only in middleware was open. It was fixed in versions 15.2.3, 14.2.25 and back-ports.

React2Shell — CVE-2025-55182, December 2025

React Server Components send data between browser and server in a format called Flight. On 3 December 2025 the React team disclosed that the server side did not properly validate Flight payloads. A single crafted request could make the server run the attacker's code, with no login at all. It scored the maximum 10.0.

It affected React 19's server packages and every framework built on them, including Next.js with the App Router. Cloudflare, Microsoft and others reported mass exploitation within hours of disclosure, dropping cryptominers and backdoors on unpatched servers.

Why it keeps working

How to do it

1. Check access where the data is read

Middleware is a good first gate. It must not be the only one. The route or data function that loads a record checks the user again:

js
app.get('/invoices/:id', requireLogin, async (req, res) => {
  const invoice = await db.invoice(req.params.id)
  if (!invoice || invoice.tenant !== req.user.tenant) return res.sendStatus(404)
  res.json(invoice)
})

Now a bypass of the outer layer still hits the inner one.

2. Strip internal headers at the edge

If your stack uses a header internally, your proxy or CDN should delete it from incoming requests. The Next.js advice for anyone who could not patch at once was exactly this.

3. Never turn client data into objects that can run

Parse into plain data (JSON.parse), validate its shape, then use it. Avoid any library feature that rebuilds classes, functions or references from client input.

4. Know what you run, and patch in hours

How we do it here

The site runs on a small Express server rather than a full-stack framework, which keeps the code between the internet and our routes short and readable. Roles are checked in middleware, and separately every query carries the tenant from the server-side session, so a skipped middleware still cannot reach another client's rows. Request bodies are parsed as plain JSON with size limits.

Benefits

Disadvantages

Checklist

Sources

Read this lesson as Markdown