Next.js Middleware Security: Authentication Bypass, SSRF, and Header Injection Vulnerabilities

Next.js middleware runs before authentication checks, making it an attractive target. This guide covers the most significant vulnerability patterns in middleware implementations, including route matcher bypasses, SSRF via rewrite destinations, and header injection via user-controlled values.

Next.js middleware sits at the edge of your application: it runs before routing, before authentication checks, and before most application logic. That positioning makes it a powerful tool for cross-cutting concerns like authentication, rate limiting, and geolocation. It also makes vulnerabilities in middleware disproportionately dangerous — a bypass here often means bypassing every security control downstream.

Three vulnerability classes recur consistently in Next.js middleware implementations: authentication check bypasses via route matcher confusion, SSRF via controllable rewrite destinations, and security header injection flaws. Each is exploitable. Each has a correct pattern.

Authentication Bypass via Route Matcher Confusion

The most common middleware vulnerability is applying authentication logic to the wrong set of routes. The Next.js middleware matcher configuration controls which paths trigger the middleware — but a misconfigured matcher is a bypass.

The trailing slash problem

// Vulnerable: matcher does not account for trailing slashes or path segments
export const config = {
  matcher: ['/admin', '/api/internal'],
}

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

A request to /admin/ (trailing slash), /admin/users, or /admin%2Fusers (URL-encoded) may not match /admin depending on the Next.js version and configuration. The matcher syntax supports regex — always use it:

// Safe: covers the path and all sub-paths
export const config = {
  matcher: ['/admin/:path*', '/api/internal/:path*'],
}

The public path exclusion problem

The inverse pattern — explicitly excluding public paths while protecting everything else — is more dangerous when it goes wrong:

// Vulnerable: exclusion list is never complete
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|public|login).*)',
  ],
}

An attacker who finds a path not in the exclusion list that still renders sensitive content bypasses authentication entirely. The risk is higher in large applications where routes accumulate over time and the middleware exclusion list is not systematically maintained.

The safer architecture separates authentication enforcement from route listing. Instead of maintaining an exclusion list, verify the session on every matched route and build a list of routes that are explicitly public:

const PUBLIC_PATHS = new Set(['/login', '/signup', '/api/auth/callback', '/about'])

export function middleware(request: NextRequest) {
  const path = request.nextUrl.pathname
  if (PUBLIC_PATHS.has(path) || path.startsWith('/_next/') || path.startsWith('/static/')) {
    return NextResponse.next()
  }
  const token = request.cookies.get('session')?.value
  if (!token || !isValidToken(token)) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

The x-middleware-subrequest bypass (CVE-2025-29927)

In versions prior to 15.2.3, 14.2.25, and 13.5.9, Next.js middleware could be bypassed by setting the x-middleware-subrequest header. This was a critical authentication bypass that allowed unauthenticated access to any route protected by middleware. If you are running an older Next.js version, upgrade immediately — this vulnerability has public exploits.

SSRF via Controllable Rewrite Destinations

Middleware rewrites redirect the request to a different URL before the application sees it. If the rewrite destination incorporates user-controlled input, the result is a server-side request forgery vulnerability.

Dynamic rewrite destinations

// Vulnerable: user-controlled subdomain incorporated into rewrite destination
export function middleware(request: NextRequest) {
  const tenant = request.headers.get('x-tenant-id') || 
                 request.nextUrl.searchParams.get('tenant')
  
  if (tenant) {
    return NextResponse.rewrite(
      new URL(`/api/${tenant}/data`, request.url)
    )
  }
}

If the rewrite target allows arbitrary URL construction, an attacker can direct server requests to internal services:

GET /?tenant=../../etc/passwd HTTP/1.1
GET /[email protected]/admin HTTP/1.1

Depending on how Next.js resolves the URL and what the target service does, this may enable reading internal endpoints, cloud metadata services, or other resources accessible from the server.

Safe pattern — validate against an allowlist:

const ALLOWED_TENANTS = new Set(['acme', 'globex', 'initech'])

export function middleware(request: NextRequest) {
  const tenantParam = request.nextUrl.searchParams.get('tenant') ?? ''
  const tenant = ALLOWED_TENANTS.has(tenantParam) ? tenantParam : null
  
  if (!tenant) {
    return NextResponse.next() // use default routing
  }
  
  // Safe: tenant is validated against allowlist before use in URL
  return NextResponse.rewrite(new URL(`/api/${tenant}/data`, request.url))
}

Rewrite to external URLs

Next.js rewrites support external URLs in some configurations. An application that rewrites to an external API and incorporates user-controlled path segments creates a partial SSRF or open redirect:

// Potentially vulnerable depending on what user controls
const apiBase = process.env.API_BASE_URL // e.g., 'https://api.internal.company.com'
return NextResponse.rewrite(`${apiBase}/users/${userId}/profile`)

If userId is not sanitised, path traversal (e.g., userId = "../../admin/config") may reach unintended endpoints on the API server.

Header Injection

Middleware is a common place to set security headers. Insecure patterns include:

Reflected user input in response headers

// Vulnerable: request header value reflected into response header
export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  const requestedLocale = request.headers.get('accept-language') || 'en'
  response.headers.set('content-language', requestedLocale) // unsafe reflection
  return response
}

HTTP header values that contain CRLF sequences (\r\n) can inject additional headers into the response, potentially enabling cache poisoning or response splitting. Always strip or validate header values before setting them:

const locale = (request.headers.get('accept-language') || 'en')
  .replace(/[^a-zA-Z,;=\-\s0-9]/g, '') // strip anything that isn't a valid locale character
  .substring(0, 64)
response.headers.set('content-language', locale)

Overwriting security headers accidentally

Middleware that sets security headers (CSP, HSTS, X-Frame-Options) can conflict with headers set by the application itself. The last write wins in Next.js header resolution. Ensure your middleware header logic does not overwrite stricter application-layer headers with weaker middleware defaults.

Secrets in Middleware Edge Runtime

Next.js middleware runs in the Edge Runtime, which has a restricted API surface. A common pattern is to import environment variables for use in token validation:

const JWT_SECRET = process.env.JWT_SECRET // only available server-side

Edge Runtime environment variables work differently from Node.js environment variables. In some deployment configurations (Vercel, Cloudflare Workers), process.env variables are available at build time and embedded in the bundle. This is not a Node.js process-level secret — depending on your deployment target, the secret may be statically embedded in the edge function bundle, potentially exposable through bundle analysis.

Review how your deployment target handles environment variable injection for edge functions. For Vercel, environment variables marked as “Server” are injected at runtime and not embedded. For self-hosted deployments, ensure your build and deployment pipeline treats edge function bundles as potentially readable artifacts.

Audit Checklist

For any Next.js application with middleware authentication:

  • Route matcher covers all protected paths including trailing slashes and sub-paths (:path*)
  • Public path list is explicitly maintained and reviewed at each deployment
  • Next.js version is at or above 15.2.3 / 14.2.25 / 13.5.9 (CVE-2025-29927 patch)
  • Rewrite destinations do not incorporate unvalidated user input
  • Response header values derived from request input are stripped and length-limited
  • Edge Runtime environment variable handling matches deployment target guarantees
  • Middleware authentication logic is covered by integration tests that verify bypass attempts are rejected

References