FlawOpen
● Serverless Request Isolation

Serverless State Bleed: Keep User Data Request-Scoped

Learn how reused serverless runtimes can expose stale user data, and how to isolate authentication and tenant state per request.

💡 Plain English Explainer (ELI5)

Treat authentication, tenant identity, and request-specific data like a visitor badge: it belongs to one visit and must not be left on a shared workbench. Some serverless runtimes reuse an execution environment or isolate for later requests. If code stores a user's context in module-level mutable state and a later request reads it, the application can cross its own user or tenant boundary.

Reuse is an implementation behavior, not a guarantee that a particular next user will reach the same runtime or that it will remain alive for a fixed period. A leak requires both shared execution state and a code path that carries request data forward or reads stale state. Keep per-request identity local to the request and pass it explicitly to the operations that need it.

Core Concepts & Key Terms

Execution environment reuse
A platform may keep a function runtime or isolate available and use it for later invocations; it may also retire it at any time.
Module-level state
A value initialized outside a handler. It may remain in that runtime between invocations, so it must not hold user- or tenant-specific data.
AsyncLocalStorage
Node.js API for propagating a store through one asynchronous execution context. It tracks context; it does not authenticate or authorize a user.
Cross-request data exposure
A failure where a later request reads data retained from an earlier request in a reused execution context. Reuse alone does not prove a leak.

Step-by-Step Attack Flow

Step 1

One request creates request-specific data

Request A authenticates and resolves its own user or tenant context. A bug appears if code writes that identity into a mutable variable at module scope instead of keeping it in the handler scope.

Step 2

The runtime may be reused

After the response, the platform may keep the execution environment warm. AWS documents environment reuse; Cloudflare Workers may reuse an isolate, but request routing and isolate lifetime are not a fixed user-to-user sequence.

Step 3

A later request reaches the same runtime

If request B is handled by that same process or isolate, any module-level value left by request A remains available unless code overwrites or clears it. An early return or missing authentication check can make stale state reachable.

Step 4

The application returns data under the wrong identity

If the handler uses that stale value for a database query or response, request B may receive data associated with request A. This is a conditional application-level isolation failure, not an inevitable property of serverless platforms.

Source Code: Flaw vs. Secure Implementation

UNSAFE: SHARED REQUEST STATE
// UNSAFE: request identity is mutable process-wide state.
import { NextResponse } from "next/server";

let cachedUserContext: { userId: string; role: string } | null = null;

export async function POST(req: Request) {
  const token = req.headers.get("authorization");

  if (token) {
    cachedUserContext = await decodeUser(token);
  }

  // If token is missing, stale state can still be used.
  return NextResponse.json({ actor: cachedUserContext?.userId });
}
SAFER: REQUEST-LOCAL IDENTITY
// SAFE PATTERN: keep identity local and pass it to the service.
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const user = await authenticate(req);
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const account = await loadAccountForUser(user.id);
  return NextResponse.json({ actor: user.id, account });
}

// The service enforces ownership in its query:
async function loadAccountForUser(userId: string) {
  return db.account.findFirst({ where: { ownerId: userId } });
}

Engineering Hardening Checklist

← Browse Full Security Directory Explore Reference Blueprints →

References

See also PHP == and Magic Hash Comparisons Is Rust's unsafe block still memory safe? Is strncpy() safer than strcpy()? CWE vs. CVE: What's the Difference? Mass Assignment: Risks and Safe Field Allowlists