Next.js and the corrupt middleware: the authorizing artifact
37 comments
·March 23, 2025kfarr
simonw
"initial response received from the Vercel team explaining that versions 12.x were no longer supported/maintained"
That doesn't mean they shouldn't issue an alert to developers still running those versions advising them to upgrade ASAP.
nine_k
OK, tangentially: let's assume that Next is poorly maintained; what are some good alternatives? Of course everything that Next does can be assembled by hand from various smaller modules on top of Express, or similar. What are some more cohesive sets?
solardev
I don't think there's anything quite as featureful. For basic sites, Astro is fine, but it's not as powerful. Vite can be used for basic client or server side renders. Nuxt and SvelteKit have some of the basic features.
But I don't think there is a drop-in replacement for ALL that Next does. The strength of Next is in packaging together what would otherwise be like twenty different packages and servers (especially if you make use of all the Vercel specific features). And then it adds incredibly powerful (but often complex) hybrid caching strategies that combine what would traditionally be done by different daemons altogether (a KV store, a memory cache, a HTTP cache, CDN. etc.). And then it adds a bunch of additional features like the middleware layer, image processing and caching, etc. I don't know of any other frontend-focused JS framework with such features in one package.
These are more common in the full-stack world. Next takes some of those traditional backend concerns and puts them in the hands of frontend devs, for better or worse. If you know a bit of both, it can be a great shortcut. If you overestimate your ability/knowledge, it can be a great footgun.
the_mitsuhiko
> But I don't think there is a drop-in replacement for ALL that Next does.
The entirety of vite + tanstack (in particular the upcoming tanstack-start) is getting quite close. For quite a few uses that folks currently use next fork, I would argue that much of what tanstack does is a better fit. Eg: non marketing sites, but SaaS style dashboards.
johnnyAghands
What is intersting?
edoceo
The lag & missing key details
johnnyAghands
Ah ok, yeah.. unfortnutely this type of lag/mismgmt is pretty common once a company gets big enough. Often times the right people don't get involved on first-pass... even at tech-first companies like this -- though at that point perhaps you're no longer tech-first :/
iterateoften
Maybe im misunderstanding how people are building endpoints these days, but every post about this I see how it can bypass auth.
Wouldn’t this bypass auth only for sites where auth is true/false?
I’ve never worked on a site were auth is a boolean. Auth is always a relative. The middleware is only there to identify the user. Then when querying for objects, you query objects related to that user.
Or if you are serving an admin page you check that the user is an admin.
I honestly find it more astounding that people put an admin security check to check the url of a page and redirect away in a middleware and no security on the views themselves.
Is this form of checking paths in middleware officially from NextJS or did people just get lazy? Seems like the worst way to build auth I could ever dream up across any framework or language.
If a middleware is bypassed all endpoints should return empty responses. In my nextjs apps the middleware is simply a convenience method for the user if they are logged out they get redirected to the login page. But all api endpoints check for the active user and serve objects relative to the user.
flufluflufluffy
> “Is this form of checking paths in middleware officially from NextJS or did people just get lazy?”
This is a common way to implement auth in many frameworks, not just JS ones. Off the top of my head I know that Laravel (PHP) does it this way. I guess you could call it “being lazy” but most people would refer to it using the age-old software engineering terms “don’t repeat yourself” and “separation of concerns.”
The authentication middleware can choose to redirect or not based on many things: did the user provide the correct credentials, does the user have the necessary permissions to access the requested resource, etc… And you can put all the logic for determining those many things in a single place, so that it can easily be updated. Individual routes can remain as they are, and you don’t have to worry about forgetting to implement some part of the auth checking logic on one of them.
Etheryte
This attack isn't only about auth, auth is just the most drastic obvious example. Middleware is often used for a wide range of sanity checks. You could bypass, say, limit checks and ask the server to return an infinite number of items per page, quickly overloading the server and resulting in DoS.
ljm
I don’t know how to put this any other way but my experience with NextJS or just JS-first full stack is that they are still first and foremost a tech stack for frontend devs and the backend piece in that context is an afterthought.
I’ve worked on a few in my time and ‘API routes’ were rarely, if ever, authenticated, and there wouldn’t be a consistent strategy for data access. If anything, everything was built in the context of satisfying a react hook and getting on with the UI.
But I don’t squarely blame developers for it, it’s more like an inverted full stack where the browser is first class and the serverless edge SSR ISG SSG app router component craziness does not help you build out a stable API. Does sell a hell of a lot of SaaS though.
simonw
How serious this vulnerability is depends entirely on how the site that's being attacked uses middleware. The auth thing is just the most obvious example of how an attacker can do bad things if they have the ability to selectively disable middleware by passing names as a colon separated list in an HTTP header.
(I've built sites that would have been affected by this in the past, had I used Next and middleware for auth. I've worked on plenty of systems where there are only a small set of users each with the same level of permissions - gating private documentation for example.)
fabian2k
Probably depends on the complexity of the permissions in the application. I'd also expect something more along the lines you described for more complex applications. The middleware would do authentication, but then just attach that information to the request. Later parts would then use the attached information to make decisions about permissions/authorization.
In more complex cases this would be outside middleware, so it should fail as no authentication/authorization information is attached to the request if you skip that middleware.
But putting the security checks into middleware could easily make sense for more rigid or simple cases. In C# for example I can add attributes to the methods that handle each endpoint. So if you need a basic admin/no-admin check you could add a [RequireAdmin] attribute on the relevant endpoints and use a middleware to check that.
I would agree that checking the URL in middleware to make decision about permissions would be a bad idea, it moves this important check to a mostly invisible place.
This probably also allows different attacks, e.g. skipping middleware that does other security-relevant checks (maybe anti-CSRF mechanisms could be vulnerable here).
hombre_fatal
I don’t think it really matters. All you have to do is write middleware or handlers that assume upstream middleware have run, and then that’s vulnerable to this attack.
For example it’s common to write middleware on /admin so that all of your /admin/* handlers don’t have to repeat the same authz logic. And the platform breaking invariants that you should be able to depend on is why it’s a security bug.
simonw
The vulnerability can be understood through this code snippet:
const subreq = params.request.headers['x-middleware-subrequest'];
const subrequests = typeof subreq === 'string' ? subreq.split(':') : [];
// ...
for (const middleware of this.middleware || []) {
// ...
if (subrequests.includes(middlewareInfo.name)) {
result = {
response: NextResponse.next(),
waitUntil: Promise.resolve(),
};
continue;
}
}
Pass an x-middleware-subrequest HTTP header with a colon-separated list of middleware names to skip.https://github.com/vercel/next.js/blob/v12.0.7/packages/next...
solid_fuel
What would this feature ever be used for? I'm surprised such a thing exists, instead of simply defining a different set of routes for a different set of middleware
nine_k
What surprises me here is that the client side of the request / response is not considered a cunning, bitter enemy, as it should be. Why is x-middleware-subrequest even accepted in production? Why is x-middleware-rewrite even returned? They are instrumental to the attack, and the client has no business accessing them, ever, in my book.
If these headers are only expected to be available within a trusted zone, and some fronting HTTP server should strip them from incoming requests and outgoing responses, why are they named like regular HTTP headers, and not in some scary, easy-to-filter-way, like x-INTERNAL-ONLY-middleware-something?
To my mind, the server should accept the bare minimum of headers needed to serve the request, and issue the minimum amount of headers to provide a well-formed response, while being completely opaque to the client. Any nifty diagnostics like x-middleware-rewrire belong to the logs; correlate by request ID. Any nifty internal processing tweaks in plain text, like x-middleware-subrequest, are, to my mind, bad architecture. If you need to pass such info between HTTP endpoints internally, use something like a JWT.
kawsper
Does anyone know which versions of Next.js that is supported?
I don't seem to be able to find a promise from Vercel, but https://endoflife.date/nextjs mentions that 15 and 14 gets security support.
soulchild77
This very recent PR updates the docs to basically remove all common (and previously recommended) middleware use-cases, rendering them almost completely useless:
rohan_
Most don't understand this issue:
Auth middleware is used for _routing_ (e.g. if you're not signed-in, you'll be redirected to the sign-in page).
This just means a 500 is thrown due to the auth() call returning null on the server.
simonw
That depends entirely on how you implemented your middleware.
This vulnerability also isn't explicitly about auth: it's about attackers being able to send a colon separated list of middleware to skip. That could affect applications in all kinds of unexpected ways depending on what they are using middleware for and how they designed their application.
johnnyAghands
Can someone tl;dr: why there is even logic to bypass middleware in the first-place, I feel like I'm missing something obvious here...
simonw
It's quite common for server-side web frameworks to send a single request through their stack multiple times / especially when there is any form of "middleware" concept involved.
Often there's a need to skip some middleware on the second or third time through.
I've built systems in the past that do all sorts of re-dispatching.
One example: in development my API might live at /api/... but in production I might use api.my.site - with middleware that detects that host, rewrites the incoming request to add that /api/ prefix and then runs it though the stack again.
Authentication is a very common way this pattern is applied - check cookies / authorization headers / whatever, then add the authenticate user to the request somehow and re-dispatch the request through the stack so other layers can see who the user is.
the_mitsuhiko
While you are generally right here I wonder how common this is with middlewares. Many have order dependencies and there are normally no loops involved. I don’t think I have come across this for middlewares at least. Kinda curious about the particular motivation here.
simonw
Yeah I've been contemplating this with my own Datasette project recently: it doesn't have an official mechanism for "redispatch this request from the root again" but I've been tempted to add one.
My GraphQL plugin for example works by firing off internal requests against Datasette's REST API and I ended up needing some gnarly hacks to get authentication to work with that.
johnnyAghands
Yeah that was my understanding as well —- but I’m not a framework author so wasn’t sure if this was a common practice.
Trade-offs aside, I personally find the idea of re-running the request through the stack a bit hacky.
ljm
NextJS however is likely constrained by its architecture and the decision to use serverless and edge compute for the backend.
Relying on obscure headers for conditional logic this way is certainly one way to avoid bringing in an extra dependency. And the middleware concept itself is fairly primitive compared to what you could do in any server-side API.
Arguably, though, the middleware itself is being trusted as the entry-point to the API when it’s barely more than a reverse proxy. It’s not really a vulnerability if you only auth’d the middleware and not your actual routes.
teaearlgraycold
To prevent infinite loops internally.
ldjkfkdsjnv
The culture of security within FAANG could not be more opposite than the way that vercel handled this. In big tech, this would have been looked at in 48 hours, and across thousands of systems all oncalls would have been paged to do an emergency deploy. Probably within 5 days, almost the whole company would have deployed the patch.
Vercel to me seems like it is run by hype men, and the CEO is certainly technical, but these people are not in the weeds in the way they come off.
czk
Also worth noting that this commit in Dec 2024 previously added a bunch of internal headers (aside from this one) to a restricted external access list (one of them was vulnerable to SSRF) and there was never a CVE for it.
tengbretson
Maybe they based their on-call protocol on what people say they want in hn threads.
null
Timeline is interesting
Timeline:
02/27/2025: vulnerability reported to the maintainers (specifying that only versions between 12.0.0 and 12.0.7 were vulnerable, which was our understanding at the time)
03/01/2025: second email sent explaining that all versions were ultimately vulnerable, including the latest stable releases
03/05/2025: initial response received from the Vercel team explaining that versions 12.x were no longer supported/maintained (probably hadn’t read the second email/security advisory template indicating that all were vulnerable)
03/05/2025: another email sent so that the team could quickly take a look at the second email/security advisory template
03/11/2025: another email sent to find out whether or not the new information had been taken into account
03/17/2025: email received from the Vercel team confirming that the information had been taken into account
03/18/2025: email received from the Vercel team: the report had been accepted, and the patch was implemented. Version 15.2.3 was released a few hours later, containing the fix (+backports)
03/21/2025: publication of the security advisory