Next.js and the corrupt middleware: the authorizing artifact

Published in zhero_web_security, 2025

Introduction

Recently, Yasser Allam, known by the pseudonym inzo_, and I, decided to team up for some research. We discussed potential targets and chose to begin by focusing on Next.js (130K stars on github, currently downloaded + 9,4 million times per week), a framework I know quite well and with which I already have fond memories, as evidenced by my previous work. Therefore, the “we” throughout this paper will naturally refer to the two of us.

Next.js is a comprehensive javascript framework based on React, packed with numerous features — the perfect playground for diving into the intricacies of research. We set out, fueled by faith, curiosity, and resilience, to explore its lesser-known aspects, hunting for hidden treasures waiting to be found.

It didn’t take long before we uncovered a great discovery in the middleware. The impact is considerable, with all versions affected, and no preconditions for exploitability — as we’ll demonstrate shortly.

Index

The Next.js middleware

Middleware allows you to run code before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly (Next.js documentation).

Next.js, being a complete framework, has its own middleware — an important and widely used feature. Its use cases are numerous, but the most important ones include:

  • Path rewriting
  • Server-side redirects
  • Adding elements such as headers (CSP, etc.) to the response
  • And most importantly: Authentication and Authorization

A common use of middleware is for authorization, which involves protecting certain paths based on specific conditions.

Authentication and Authorization: Ensure user identity and check session cookies before granting access to specific pages or API routes (Next.js documentation).

Exemple: When a user attempts to access /dashboard/admin, their request will first go through the middleware, which will check if their session cookies are valid and grant them the necessary permissions. If so, the middleware will forward the request; otherwise, the middleware will redirect the user to a login page:

The authorizing artifact: old code, 0ld treasure

As a great man once said, talk is cheap, show me the bug, let’s avoid the excess storytelling and get straight to the point; while browsing an older version of the framework (v12.0.7), we came across this piece of code:

When a next.js application uses a middleware, the runMiddleware function is used, the latter - beyond its main utility - retrieves the value of the x-middleware-subrequest header and uses it to know if the middleware should be applied or not. The header value is split to create a list using the column character (:) as a separator and then checks if this list contains the middlewareInfo.name value.

This means that if we add the x-middleware-subrequest header with the correct value to our request, the middleware - whatever its purpose - will be completely ignored, and the request will be forwarded via NextResponse.next() and will complete its journey to its original destination without the middleware having any impact/influence on it. The header and its value act as a universal key allowing rules to be overridden. At this point we already know that we have just unearthed something crazy, we now have to complete the last pieces.

For our “universal key” to work, its value must contain middlewareInfo.name, but what is it?

Execution order and middlewareInfo.name

The value of middlewareInfo.name is perfectly guessable, it is only the path in which the middleware is located. And to know this, it is necessary to take a quick detour to understand how the middleware was configured in older versions.

To begin with, before version 12.2 - version during which a change in middleware conventions was made - the file had to be named _middleware.ts. Furthermore, the app router was only released in version 13 of Next.js. The only router that existed at that time was the pages router, so the file had to be placed inside the pages folder (router specific).

This information allows us to deduce the exact path of the middleware, and therefore to guess the value of the x-middleware-subrequest header, the latter is simply composed of the name of the directory (having the name of the only existing router existing at time T) and the name of the file, respecting the convention of that time consisting of starting with an underscore:

x-middleware-subrequest: pages/_middleware

And when we try to bypass our middleware configured to systematically redirect access attempts to /dashboard/team/admin to /dashboard:

akhy, we are in ⚔️

We can now completely bypass the middleware, and therefore any protection system based on it, starting with authorization, as in our example above. It’s pretty crazy, but there are other points to consider.

Versions prior to 12.2 allowed nested routes to place one or more _middleware files anywhere in the tree (starting from the pages folder) and had an execution order, as we can see in this screenshot of the old documentation retrieved from the good old web archive:

Ok, what does this mean for our exploit?

possibilities = numbers of levels in the path

So, to gain access to /dashboard/panel/admin (protected by middleware), there are three possibilities regarding the value of middlewareInfo.name, and therefore of x-middleware-subrequest:

pages/_middleware

or

pages/dashboard/_middleware

or

pages/dashboard/panel/_middleware

The authorizing artifact: nostalgia has its charm, but living in the moment is better

Until now, we thought that only versions prior to version 13 were vulnerable because the middleware had been moved in the source code, and we hadn’t yet covered some of its areas. We figured they must have noticed the vulnerability and fixed it before the major changes made in version 13, so we reported the vulnerability to the framework maintainers and continued our research.

To our big surprise, we discovered two days after this initial discovery that all versions of next.jsstarting with version 11.1.4were vulnerable..! The code is no longer in the same location, and the exploit’s logic has changed slightly.

As explained previously, starting with version 12.2, the file no longer contains underscores and must simply be named middleware.ts. Furthermore, it must no longer be located in the pages folder (which is convenient for us because starting with version 13, the router app is introduced, and that would have doubled the number of possibilities).

With that in mind, the payload for the first versions starting with version 12.2 is very simple:

x-middleware-subrequest: middleware

/src directory

It should also be taken into account that Next.js gives the possibility to create a /src directory:

As an alternative to having the special Next.js app or pages directories in the root of your project, Next.js also supports the common pattern of placing application code under the src directory. (Next.js documentation)

In which case the payload would be:

x-middleware-subrequest: src/middleware

So, a total of two possibilities, regardless of the number of levels in the path. This simplifies the exploit for the few versions concerned.

From the latest versions, it changes a little again (last time, we promise).

Max recursion depth

On more recent versions the logic has changed slightly again, take a look at this piece of code :

v15.1.7

The value of the header x-middleware-subrequest is retrieved in order to form a list whose separator is the column character, as before. But this time, the condition for the request to be forwarded directly - ignoring the rules of the middleware - is different:

The value of the constant depth must be greater or equal than the value of the constant MAX_RECURSION_DEPTH (which is 5), when assigned, the constant depth is incremented by 1 each time one of the values ​​of the list -subrequests- (being the result of the header value separated by :) is equal to the value params.name which is simply the path to the middleware. And as explained earlier, there are only two possibilities: middleware or src/middleware.

So we just need to add the following header/value to our request in order to bypass the middleware:

x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

or

x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware

What is this piece of code originally used for?

This seems to be there to prevent recursive requests from falling into an infinite loop.

Exploits

Since we know you like this, here are some real examples from BBP programs.

Authorization/Rewrite bypass

Here, when we try to access /admin/login we get a 404. As we can see in the response header, a path rewrite is performed via the middleware to prevent unauthenticated/inappropriate users from accessing it:

But with our authorizing artifact:

We can access the endpoint without any problems, the middleware being completely ignored. Target Next.js version: 15.1.7

CSP bypass

This time the site uses the middleware to set -among other things- the CSP and the cookies :

Let’s bypass it:

Target next.js version: 15.0.3

Note: Notice the difference in payload between the two targets, one of them used the src/ directory, the other did not.

DoS via Cache-Poisoning (what?)

Yes, a cache-poisoning DoS is also possible via this vulnerability. This is obviously not what we’re looking for first, but if no sensitive paths are protected, and nothing more interesting seems exploitable, then certain situations can lead to a CPDoS:

Let’s say a site rewrites its users’ paths based on their geographic location, adding (/en, /fr, etc.), and has not provided a page/resource on the root (/). If we bypass the middleware, we consequently avoid the rewrite and we end up on the root page, the latter not being supposed to be reached, the developers did not provide a page, so we get a 404 (or a 500 depending on the config/type of rewrite (full url/path)).

If the site has a cache/CDN system, it may be possible to force the caching of a 404 response, rendering its pages unusable and significantly impacting its availability.

Clarification

Since the security advisory was released, we’ve received a few inquiries from people concerned about their applications and not understanding the scope of the attack. To be clear, the vulnerable element is the middleware. If it isn’t used (or at least isn’t used for sensitive purposes), there’s nothing to worry about (check the DoS aspect above, though), since bypassing the middleware won’t bypass any security mechanisms.

Otherwise, the consequences can be catastrophic, and we encourage you to quickly implement the security advisory’s instructions.

Security Advisory - CVE-2025-29927

Patches

  • For Next.js 15.x, this issue is fixed in 15.2.3
  • For Next.js 14.x, this issue is fixed in 14.2.25
  • For Next.js versions 11.1.4 thru 13.5.6 we recommend consulting the below workaround.

Workaround

If patching to a safe version is infeasible, we recommend that you prevent external user requests which contain the x-middleware-subrequest header from reaching your Next.js application.

Severity

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (Critical 9.1/10)

https://github.com/vercel/next.js/security/advisories/GHSA-f82v-jwr5-mffw

More

At the time of writing, applications on Vercel and Netlify are apparently no longer vulnerable (Update: Cloudflare has moved this rule to opt-in only due to numerous false positives that fail to distinguish between requests from legitimate users and those from potential attackers):

https://x.com/nextjs/status/1903522002431857063

Disclaimer

This research publication is for educational purposes only, whether to help developers understand the root causes of the problem or to inspire researchers/bug hunters in their future campaigns. This publication accompanies the security advisory and provides clarifications/explanations regarding its nature, as the latter has already revealed the header responsible for the vulnerability (the commit diff too).

We obviously disclaim any unethical use of this article.

Conclusion

As highlighted in this paper, this vulnerability has been present for several years in the next.js source code, evolving with the middleware and its changes over the versions. A critical vulnerability can occur in any software, but when it affects one of the most popular frameworks, it becomes particularly dangerous and can have severe consequences for the broader ecosystem. As mentioned earlier, Next.js is downloaded nearly 10 million times a week at the time of writing. It is widely used across critical sectors, from banking services to blockchain. The risk is even greater when the vulnerability impacts a mature feature that users rely on for essential functions like authorization and authentication.

The vulnerability took a few days to be addressed by the Vercel team, but it should be noted that once they became aware of it, a fix was committed, merged, and implemented in a new release within a few hours (including backports).

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

To conclude, the search for zero-day vulnerabilities is only exciting and adrenaline-pumping when a lead emerges; the rest of the time, it’s an uncertain journey—rewarding in knowledge for the curious and relatively long for the impatient. Don’t hesitate to team up; desert crossings are easier to navigate in groups.

Further research in the pipeline, follow us on our respective X accounts to stay connected.

Thank you for reading.

Al hamduliLlah;

zhero; & inzo_