If you’re using Next.js’ Middleware for authorization, you’re doing something wrong
Whenever the topic of authorization in our Next.js apps came up at Gigs, I had a very strict opinion and rule: we don’t use the middleware for authorization. We can use the middleware for some optimistic UI patterns, like an early redirect when a user is logged out, but never as a means to grant a user access to some data. I’m not saying this because I hate the middleware, or because it’s an easily predictable vulnerability, but because of the way the Next.js middleware sits in an application.
If you were affected by CVE-2025-29927 in the sense that you were vulnerable to an authorization bypass, you should rethink the structure of your application. Reading the responses to this CVE makes me think that people either don’t know how Next.js’ middleware works, or they don’t know where authorization should happen in an application.
The Next.js middleware is very bare-bones: you have a single function for your entire application that gets a NextRequest
and you return a NextResponse
. The response can be a rewrite()
or a redirect()
or next()
, the latter meaning “continue routing”. But unlike other web frameworks, Next.js’ middleware can’t write to a context object that you can read from later in your routes. Your routes only have access to the NextRequest
, which you can’t extend however you want. You can only add headers to the request in the middleware, but an HTTP header is obviously not something you should trust blindly.
In other web frameworks, the middleware writes data to a context object that you can control and can trust; it should be impossible for anyone outside to write to this object. In an Express middleware, you would authenticate the user and then add the current user to req.user
, in Koa you would add it to ctx.user
. Your routes will then do authorization checks based on this context, or you may even have another middleware layer that returns a 401 Unauthorized
if the user context is missing.
In these examples, if your authentication middleware failed or was skipped, the context object would be missing, and the routes would subsequently fail. But if your Next.js middleware can fail or be skipped, and your routes just keep working, there’s something seriously wrong with your software design.
Your authorization logic should happen, with your authentication context (i.e. the current user object), close to where you handle data: either after you fetch some data and before you return it to the user, or, if possible, even before you fetch the data. But whether you do it before or after you fetch data, it happens in or very close to your data layer. However, as you might notice, Next.js’ middleware is nowhere near an authorization check in the data layer. I treat it as part of the presentation layer.
Relying on authorization within Next.js’ middleware is like building a single-page application with a login, but the API that you use when the user is logged in has no authorization at all. In this example, you also have no shared context that you can fully trust.
In our Next.js apps at Gigs, we split our codebase into a presentation layer and a data layer. The presentation layer (the /app
folder) can never access data directly by calling an API or a database, instead it has to call a function in the data layer. Inside the data layer (the /data
folder), we have a userContext()
function that returns the acting user by reading it from the request’s encrypted HTTP-Only cookie. The userContext()
function is only allowed to be called within the data layer, and not from the presentation layer. We use this user context object every time we try to access data in the data layer, before we return any data back to the presentation layer.
That’s how we make sure the authorization happens at the right place. How you structure your code may be different, but however you structure it, you should still put authorization close to where you’re accessing data.