NEXTJS CVE-2025-29927
CVE Analysis
In this blog post, we perform an analysis of a specific Next.js setup and its associated Common Vulnerabilities and Exposures (CVE) concerns. We’ll walk through the setup process, explore the project’s file structure, and dive deep into the call graph of Next.js to understand how requests are handled and how middleware plays a crucial role.
Setup
First, create a new Next.js app with the vulnerable version and prepare your debugging environment:
$ npx create-next-app@12.0.7 debug-app
$ cd debug-app
$ rm -rf node_modules package-lock.json .git
Update package.json, to ensure you’re using the vulnerable version of Next.js (12.0.7):
{
"name": "debug-app",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "12.0.7", // <- vulnerable version
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"eslint": "8.4.1",
"eslint-config-next": "12.0.7"
}
}
After updating, install the dependencies with:
npm install
Debugging Configuration
For debugging, add the following configuration to .vscode/launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": {
"action": "debugWithChrome",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}"
}
}
]
}
Next.js Configuration
Edit next.config.js
to enable production browser source maps:
module.exports = {
productionBrowserSourceMaps: true,
}
If you have a tsconfig.json
, ensure you add "sourceMap": true:
{
"compilerOptions": {
"sourceMap": true, // <-------------- ADD
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}
Project File Structure
Here’s an overview of the current file structure (excluding node_modules):
$ tree . -I "node_modules"
.
├── README.md
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│ ├── _app.js
│ ├── dashboard
│ │ ├── admin
│ │ │ └── index.tsx
│ │ └── index.tsx
│ └── index.js
├── public
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ └── globals.css
└── tsconfig.json
Note:
Since we are using Next.js 12.0.7, the middleware file must be named
middleware.js
and placed in the project root. In newer versions, the middleware file is named_middleware.js
and is located in the pages folder.The pages folder is where the frontend source code resides (e.g., index.tsx, home.js, dashboard.tsx, etc.).
Files added:
pages/dashboard/index.tsx
pages/dashboard/admin/index.tsx
middleware.ts
Middleware Implementation
Below is the content of middleware.ts which demonstrates how to redirect requests from /dashboard/admin
to /
:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
console.log("Middleware triggered");
if (request.nextUrl.pathname === '/dashboard/admin') {
console.log("Redirecting from /dashboard/admin to /");
return NextResponse.redirect(new URL('/', request.url));
}
return NextResponse.next(); // Ensure other routes continue
}
export const config = {
matcher: '/dashboard/admin', // Only run middleware for this route
};
Next.js Call Graph Analysis
Understanding the Next.js call graph is essential for debugging and vulnerability analysis. Here’s a high-level overview of the process:
start-server.js:requestListener(req, res) <--- request start here
1. router-server.js:handleRequest()
1.1 this gets the resolver for the route from resolve-router.js:resolveRoutes() (resolves the route path and get the to file to run)
handle the routes in following manner , based of resolve-router.js it runs follown
0 = {match: ƒ, name: 'middleware_next_data'}
1 = {source: '/:path+/', destination: '/:path+', permanent: true, locale: undefined, internal: true, …}
2 = {match: ƒ, name: 'middleware'} <---- runs middelware
3 = {match: ƒ, name: 'before_files_end'}
4 = {match: ƒ, name: 'check_fs'} check_fs <---- looks for file , .tsx , .js etc and return file that matches path
5 = {check: true, match: ƒ, name: 'after files check: true'}
-
1.1.1 all these handlers run one after another and few of them calls base-server.js:`handleRequestImpl` <---- this is most important
this function evaltes multiple state of request, midleware also calls this, render also calls this :
if req["middlewareInvoke"] -> calls middleware executer -> `next-server.js:handleCatchallRenderRequest`
if req["invokePath"] -> calls renderer -> `next-server.js:handleCatchallMiddlewareRequest`
-
1.2 await invokeRender()(render the file)
next-server:handleCatchallMiddlewareRequest() -> next-server:this.runMiddleware({ request: req, response: res, parsedUrl: parsedUrl, ...});
next-server:runMiddleware() -> next-server:run({ distDir: this.distDir, name: middlewareInfo.name, paths: middlewareInfo.paths, ... });
sandbox.js:run() ->
sandbox.js:
const run = withTaggedErrors(async function runWithTaggedErrors(params) {
var _params_request_body;
const runtime = await getRuntimeContext(params);
const subreq = params.request.headers[`x-middleware-subrequest`];
const subrequests = typeof subreq === 'string' ? subreq.split(':') : [];
const MAX_RECURSION_DEPTH = 5;
const depth = subrequests.reduce((acc, curr)=>curr === params.name ? acc + 1 : acc, 0);
if (depth >= MAX_RECURSION_DEPTH) {
return {
waitUntil: Promise.resolve(),
response: new runtime.context.Response(null, {
headers: {
'x-middleware-next': '1'
}
})
};
}
...
simply said, sending
HEADER
: x-middleware-subrequest: params.name:params.name:params.name:params.name:params.name
sending same value in this header more than 5 time will stop this middleware from running thus bypassing the middleware to run.
Key Parameter : params.name
The value of params.name represents the middleware path. In Next.js versions prior to 13, it can only be either middleware
or src/middleware
.
In newer versions, the naming convention changes slightly, but the analysis here is based on version 12.0.7.
Conclusion This blog post has walked through the setup and configuration of a Next.js project vulnerable to a specific CVE, explained the directory structure, detailed the middleware implementation, and analyzed the call graph for request handling. Understanding this flow is crucial not only for debugging but also for performing a thorough vulnerability analysis. By recognizing how middleware intercepts and processes requests, developers can better secure their applications and address potential weaknesses.
Feel free to experiment with the setup, review the call graph, and dive deeper into the middleware sandbox implementation to further enhance your security analysis.
Happy coding!
Last updated