Serverless Concurrency

Serverless State Bleed: Why Module-Level Variables in Next.js & Lambda Leak User Data Across Requests

How warm container reuse in Vercel, AWS Lambda, and Cloudflare Workers causes global variable cross-contamination, leaking private sessions across unrelated users.

💡 Plain English Explainer (ELI5)

In traditional servers, code runs for hours. In serverless, you might think every HTTP request gets a brand-new computer that instantly disappears. In reality, cloud providers keep containers 'warm' to save time. If you save user info in a top-level variable like `let currentUser = null`, User A's email stays in memory—and User B sees User A's private dashboard two seconds later.

Core Concepts & Key Terms

Warm Container Reuse
Serverless providers keep execution containers running for minutes after a request to avoid cold-start latencies.
Module-Level Singleton State
Variables declared outside the request handler function that persist across subsequent invocations within the same process.
AsyncLocalStorage
Node.js asynchronous state tracking primitive that stores data bound to the lifecycle of a specific async execution chain.
Multi-Tenant Cross-Contamination
A critical security flaw where private tenant data is served to another client due to shared in-memory variables.

Step-by-Step Attack Flow

Step 1

1. User A Initiates Request

User A hits /api/profile. The serverless cold start executes and populates a top-level variable: currentSession = userA_data.

Step 2

2. Container Kept Warm

The serverless function responds with 200 OK. The Lambda/Vercel instance remains idle in memory for 5 minutes waiting for the next invocation.

Step 3

3. User B Hits Endpoint

User B makes an unrelated request to the same worker instance. Due to missing re-initialization or an early conditional exit, the function references the warm module variable.

Step 4

4. Silent Data Leak

The endpoint serializes currentSession, returning User A's private profile, tokens, or billing history directly to User B.

Source Code: Flaw vs. Secure Implementation

VULNERABLE PATTERN
// VULNERABLE: Next.js Route Handler with Module-Level Shared State
import { NextResponse } from "next/server";

// CRITICAL FLAW: This variable lives in the Node.js process module scope!
// It persists across multiple requests inside the same warm container!
let cachedUserContext: { userId: string; role: string } | null = null;

export async function POST(req: Request) {
  const token = req.headers.get("authorization");
  
  if (token) {
    // Overwrites global state for this container
    cachedUserContext = await decodeUser(token);
  }
  
  // If an unauthenticated request comes in immediately after, 
  // cachedUserContext still contains the previous user's credentials!
  return NextResponse.json({ 
    message: "Action processed", 
    actor: cachedUserContext?.userId 
  });
}
HARDENED DEFENSE
// SECURE: Strictly Request-Scoped Context via AsyncLocalStorage or Parameters
import { NextResponse } from "next/server";
import { AsyncLocalStorage } from "node:async_hooks";

// Store state strictly isolated to the async call tree of each request
const requestStorage = new AsyncLocalStorage<{ userId: string; role: string }>();

export async function POST(req: Request) {
  const token = req.headers.get("authorization");
  if (!token) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const currentUser = await decodeUser(token);

  // Run downstream logic within isolated per-request storage
  return requestStorage.run(currentUser, async () => {
    const activeUser = requestStorage.getStore();
    return NextResponse.json({ 
      message: "Action processed", 
      actor: activeUser?.userId 
    });
  });
}

Engineering Hardening Checklist

← Browse Full Security Directory Explore Reference Blueprints →