How warm container reuse in Vercel, AWS Lambda, and Cloudflare Workers causes global variable cross-contamination, leaking private sessions across unrelated users.
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.
User A hits /api/profile. The serverless cold start executes and populates a top-level variable: currentSession = userA_data.
The serverless function responds with 200 OK. The Lambda/Vercel instance remains idle in memory for 5 minutes waiting for the next invocation.
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.
The endpoint serializes currentSession, returning User A's private profile, tokens, or billing history directly to User B.
// 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
});
}
// 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
});
});
}
let, var, module-level arrays or maps) outside serverless request handlers.AsyncLocalStorage or React cache() to encapsulate per-request authentication context.