NextJS

Reading time: 23 minutes

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

General Architecture of a Next.js Application

Typical File Structure

A standard Next.js project follows a specific file and directory structure that facilitates its features like routing, API endpoints, and static asset management. Here's a typical layout:

lua
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/: Contains API routes, allowing you to create serverless functions that handle HTTP requests. These routes replace the traditional pages/api directory.
  • 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 environment variables specific to the local development environment. These variables are not committed to version control.
  • 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:

arduino
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:

tsx
tsxCopy code// 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:

arduino
arduinoCopy codemy-nextjs-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ about/
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
β”œβ”€β”€ public/
β”œβ”€β”€ next.config.js
└── ...

Implementation:

tsx
// 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:

arduino
arduinoCopy codemy-nextjs-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ posts/
β”‚   β”‚   └── [id]/
β”‚   β”‚       └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
β”œβ”€β”€ public/
β”œβ”€β”€ next.config.js
└── ...

Implementation:

tsx
tsxCopy code// 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:

arduino
arduinoCopy codemy-nextjs-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ dashboard/
β”‚   β”‚   β”œβ”€β”€ settings/
β”‚   β”‚   β”‚   └── profile/
β”‚   β”‚   β”‚       └── page.tsx
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
β”œβ”€β”€ public/
β”œβ”€β”€ next.config.js
└── ...

Implementation:

tsx
tsxCopy code// 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:

arduino
my-nextjs-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ [..slug]/
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
β”œβ”€β”€ public/
β”œβ”€β”€ next.config.js
└── ...

Implementation:

tsx
// 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:

jsx
// 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:

jsx
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

It's a vulnerability that allows attackers to manipulate client-side paths to perform unintended actions, such as Cross-Site Request Forgery (CSRF). Unlike server-side path traversal, which targets the server's filesystem, CSPT focuses on exploiting client-side mechanisms to reroute legitimate API requests to malicious endpoints.

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.

jsx
// 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.

Server-Side in Next.js

Server-Side Rendering (SSR)

Pages are rendered on the server on each request, ensuring that the user receives fully rendered HTML. In this case you should create your own custom server to process the requests.

Use Cases:

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

Implementation:

jsx
// 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:

jsx
// 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 allows the creation of API endpoints as serverless functions. These functions run on-demand without the need for a dedicated server.

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:

go
my-nextjs-app/
β”œβ”€β”€ app/
β”‚   └── api/
β”‚       └── hello/
β”‚           └── route.js
β”œβ”€β”€ package.json
└── ...

Implementation:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

go
goCopy codemy-nextjs-app/
β”œβ”€β”€ pages/
β”‚   └── api/
β”‚       └── hello.js
β”œβ”€β”€ package.json
└── ...

Implementation:

javascript
javascriptCopy code// 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:

bash
bashCopy codemy-nextjs-app/
β”œβ”€β”€ pages/
β”‚   └── api/
β”‚       └── users/
β”‚           └── [id].js
β”œβ”€β”€ package.json
└── ...

Implementation:

javascript
javascriptCopy code// 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:

javascript
javascriptCopy code// 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:

javascript
// 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",
    },
  })
}

Note that CORS can also be configured in all the API routes inside the middleware.ts file:

javascript
// 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: '*': Permits any website to access the API, potentially allowing malicious sites to interact with your API without restrictions.
  • Wide Method Allowance: Allowing all methods can enable attackers to perform unwanted actions.

How attackers exploit it:

Attackers can craft malicious websites that make requests to your API, potentially abusing functionalities like data retrieval, data manipulation, or triggering unwanted actions on behalf of authenticated users.

{{#ref}} ../../pentesting-web/cors-bypass.md {{#endref}}

Server code exposure in Client Side

It's can easy to use code used by the server also in code exposed and used by the client side, the best way to ensure that a file of code is never exposed in the client side is by using this import at the beginning of the file:

js
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:

typescript
// 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*"],
}

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:

javascript
// 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 images for performance, but misconfigurations can lead to security vulnerabilities, such as allowing untrusted sources to inject malicious content.

Bad Configuration Example:

javascript
// next.config.js

module.exports = {
  images: {
    domains: ["*"], // Allows images from any domain
  },
}

Problem:

  • '*': Permits images to be loaded from any external source, including untrusted or malicious domains. Attackers can host images containing malicious payloads or content that misleads users.
  • Another problem might be to allow a domain where anyone can upload an image (like raw.githubusercontent.com)

How attackers abuse it:

By injecting images from malicious sources, attackers can perform phishing attacks, display misleading information, or exploit vulnerabilities in image rendering libraries.

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:

javascript
// next.config.js

module.exports = {
  env: {
    SECRET_API_KEY: process.env.SECRET_API_KEY, // Exposed to the client
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, // Correctly prefixed for client
  },
}

Problem:

  • SECRET_API_KEY: Without the NEXT_PUBLIC_ prefix, Next.js does not expose variables to the client. However, if mistakenly prefixed (e.g., NEXT_PUBLIC_SECRET_API_KEY), it becomes accessible on the client side.

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:

javascript
// next.config.js

module.exports = {
  async redirects() {
    return [
      {
        source: "/redirect",
        destination: (req) => req.query.url, // Dynamically redirects based on query parameter
        permanent: false,
      },
    ]
  },
}

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:

bash
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:

javascript
// 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:

jsx
// 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:

jsx
// 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:

javascript
// 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 of 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:

javascript
// next.config.js
module.exports = {
  env: {
    API_KEY: process.env.API_KEY, // Accessible on both client and server
    SECRET_KEY: process.env.SECRET_KEY, // Be cautious if accessible on the client
  },
}

Note: To restrict variables to server-side only, omit them from the env object or prefix them with NEXT_PUBLIC_ for client exposure.

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:

javascript
// 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:

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

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

tip

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks