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:
- Trusting a header from the outside. The framework uses a request header internally, and forgets that anyone on the internet can send that header too.
- Deserializing untrusted input. The framework turns data from the client back into live objects, and a crafted payload turns into code.
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
- Internal signals leak out. A header, cookie or query flag meant for "us" is readable and writable by "them" unless something strips it.
- Convenience layers hide the trust boundary. Middleware and server actions feel like part of your app, so it is easy to forget they parse raw internet input.
- One check guarding everything is one bug from nothing. If the only login check lives in middleware, a middleware bypass is a total bypass.
- Patch windows are now hours. Attackers diff the fix, find the flaw, and scan before most teams read the advisory.
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:
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
- Subscribe to security advisories for your framework and its server packages.
- Keep upgrades small and frequent, so an emergency patch is a routine one.
- Have a way to deploy within the same day.
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
- Defence in depth turns a framework bug from "everything is open" into "one layer failed".
- Stripping internal headers is one line of proxy config and closes a whole class of bugs.
- Small, frequent upgrades make emergency patches boring.
Disadvantages
- Checking access in two places is repeated code, and the two can drift apart.
- A smaller framework means building yourself what a big one gives you, with your own bugs.
- Patching in hours needs good tests and a fast deploy, which takes real work to set up.
Checklist
- Every route that reads data checks the user, not only the middleware.
- Internal-only headers are stripped from incoming requests.
- No client input is turned into live objects.
- You are subscribed to your framework's security advisories.
- You can ship a patch the same day.