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:

  1. pages/dashboard/index.tsx

  2. pages/dashboard/admin/index.tsx

  3. 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