Tech Verse Logo
Enable dark mode
Preventing nextjs middleware cache poisoning

Preventing nextjs middleware cache poisoning

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

•4 min read

Middleware runs before every route match in your app. It intercepts requests, inspects cookies, evaluates headers, and decides whether to rewrite, redirect, or pass the request down to your page components. That power makes it dangerous. When you mutate incoming requests and pass untrusted values directly into response headers, you open the door to cache pollution. Upstream CDNs and edge layers store responses based on specific keys. If your middleware writes request-derived values into cacheable response headers without proper variation, you'll end up serving user A's private dashboard to user B. Don't let this happen in production.

The Anatomy of an Edge Cache Leak

CDNs and reverse proxies cache static assets and dynamic page renders to save CPU cycles and database queries. They use cache keys to determine which stored response matches an incoming request. By default, these keys rely on the request path and specific query parameters. But apps often need different behavior based on user authentication or geographic region. You might be tempted to inspect cookies in your middleware, determine the user role, and set a custom header on the request before passing it along. It's a common trap.

Here is what typical, vulnerable middleware looks like in practice. It grabs an authorization cookie, decodes a tenant identifier, and attaches it as a request header for downstream components to read.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session_token')?.value;
  
  const requestHeaders = new Headers(request.headers);
  if (token) {
    requestHeaders.set('x-tenant-id', parseTenantFromToken(token));
  }

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });
}

function parseTenantFromToken(token: string): string {
  return token.split('.')[0];
}

export const config = {
  matcher: '/dashboard/:path*',
};

The code above looks innocent enough. Downstream server components can now read the x-tenant-id header via standard header APIs instead of manually parsing cookies on every render pass. The bug occurs when an upstream proxy or CDN caches the resulting HTML page without knowing that the response varies based on that custom header. If the CDN key only tracks the URL path, the first response generated for tenant alpha gets stored in the edge cache. When tenant beta hits the exact same URL path seconds later, the CDN serves the cached page containing tenant alpha's data. You've just leaked private state entirely through improper caching assumptions.

Fixing Vary Headers and Cache Control

To prevent downstream caches from serving polluted responses, you must explicitly tell the CDN how to vary its cache keys. The Vary HTTP header tells intermediate caches that they must store separate copies of the response for every distinct value found in the listed request headers. If your app relies on user-specific headers or custom routing tokens, those keys must appear in the response's Vary directive. Understanding how data moves through different layers is critical here, much like optimizing data delivery strategies described in Next.js 16 Caching Architecture: The Four Layers. You'll find it makes a massive difference.

You also need to set explicit cache control directives on any route that handles authenticated sessions or user-specific data. Pages that change based on who is looking at them should never be stored in public caches. Setting private or no-store directives ensures that edge nodes skip caching entirely for sensitive endpoints. It's a simple fix that stops leaks cold.

Here is how you update your middleware or response handling to include the correct cache isolation headers.

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  const isPrivateData = request.nextUrl.pathname.startsWith('/dashboard');
  
  if (isPrivateData) {
    response.headers.set('Vary', 'Cookie, X-Tenant-ID');
    response.headers.set('Cache-Control', 'private, no-cache, no-store, max-age=0');
  }

  return response;
}

export const config = {
  matcher: '/dashboard/:path*',
};

Adding Vary: Cookie tells the edge that the cache entry is tied directly to the visitor's cookie jar. Even if two requests share the exact same URL path, they will never share a cache bucket unless their cookies match. Combining this with no-store on highly sensitive routes eliminates the risk of accidental cross-tenant data exposure. You don't want to skip this step.

Validating Edge Behavior in Staging

Configuring headers correctly in code is only half the battle. You have to verify that your CDN actually honors them. Local dev environments rarely expose cache poisoning vulnerabilities because local servers don't simulate the aggressive edge caching behavior of production platforms. You need to deploy your app to a staging environment and test the edge behavior explicitly using command line tools. It's the only way to be sure.

You can inspect response headers and cache tags directly using curl to ensure your caching rules work as intended. Always check for missing Vary headers on dynamic endpoints before pushing code to production environments. Combining secure middleware configurations with proper static and dynamic boundaries, as explored in Next.js Partial Prerendering: Static Shells, Dynamic Holes, helps keep your app fast without sacrificing security. Keep your middleware focused on routing logic and header sanitization. Never trust raw header inputs from external proxies without validating them against secure internal tokens or session stores.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Local Embedding Models in Production RAG

    Local Embedding Models in Production RAG

    Preventing nextjs middleware cache poisoning

    Preventing nextjs middleware cache poisoning

    Fixing Async Python Memory Leaks from Unclosed Sessions

    Fixing Async Python Memory Leaks from Unclosed Sessions

    Optimizing Inertia SSR Performance in Production

    Optimizing Inertia SSR Performance in Production

    Cron vs Systemd Timers: Which to Use

    Cron vs Systemd Timers: Which to Use

    Configuring Linux Deploy Users, Sudo, and SSH Keys

    Configuring Linux Deploy Users, Sudo, and SSH Keys

    Linux Disk Full Troubleshooting

    Linux Disk Full Troubleshooting

    Nginx Reverse Proxy for Node and PHP

    Nginx Reverse Proxy for Node and PHP

    Mastering Linux File Permissions and Web App Storage

    Mastering Linux File Permissions and Web App Storage

    Self-Hosted Linux Server Monitoring Without SaaS Fees

    Self-Hosted Linux Server Monitoring Without SaaS Fees