FlawOpen
● Isolation des requêtes serverless

Fuite d’état serverless : garder les données utilisateur par requête

Comment la réutilisation des runtimes serverless peut exposer des données périmées, et isoler identité et état par requête.

💡 Explication simple (ELI5)

Traitez l’authentification, l’identité du tenant et les données de requête comme un badge visiteur : elles appartiennent à une visite et ne doivent pas rester sur un plan de travail partagé. Certains runtimes serverless réutilisent un environnement ou un isolate pour des requêtes ultérieures. Si le code conserve le contexte d’un utilisateur dans un état mutable au niveau du module et qu’une autre requête le lit, l’application peut franchir sa propre frontière entre utilisateurs ou tenants.

La réutilisation est un comportement d’implémentation, pas la garantie que l’utilisateur suivant atteindra le même runtime ni que celui-ci restera actif pendant une durée fixe. Une fuite exige à la fois un état d’exécution partagé et un chemin de code qui conserve les données de la requête ou lit un état périmé. Gardez l’identité dans la requête et transmettez-la explicitement aux opérations qui en ont besoin.

Concepts clés et termes

Execution environment reuse
Réutilisation de l’environnement : la plateforme peut garder un runtime ou isolate disponible pour d’autres invocations, mais peut aussi l’arrêter à tout moment.
Module-level state
État au niveau du module : valeur initialisée hors du handler. Elle peut subsister entre invocations dans ce runtime et ne doit pas contenir de données propres à un utilisateur ou tenant.
AsyncLocalStorage
AsyncLocalStorage : API Node.js qui propage un store dans un contexte asynchrone. Elle suit le contexte ; elle n’authentifie ni n’autorise un utilisateur.
Cross-request data exposure
Exposition inter-requêtes : une requête ultérieure lit des données conservées par une précédente dans le même contexte réutilisé. La réutilisation seule ne prouve pas une fuite.

Flux d’exposition des données, étape par étape

Step 1

Une requête crée ses propres données

La requête A s’authentifie et résout son contexte utilisateur ou tenant. Le défaut apparaît si le code écrit cette identité dans une variable mutable au niveau du module au lieu de la garder dans le handler.

Step 2

Le runtime peut être réutilisé

Après la réponse, la plateforme peut garder l’environnement actif. AWS documente cette réutilisation ; Cloudflare Workers peut réutiliser un isolate, mais le routage et la durée de vie ne définissent pas une séquence fixe entre utilisateurs.

Step 3

Une requête ultérieure atteint le même runtime

Si la requête B est traitée par le même processus ou isolate, la valeur de module laissée par A reste disponible jusqu’à son remplacement ou sa suppression. Un retour anticipé ou une authentification manquante peut rendre l’ancien état accessible.

Step 4

L’application renvoie des données sous la mauvaise identité

Si le handler utilise cette valeur périmée pour une requête de base de données ou une réponse, B peut recevoir des données associées à A. C’est un défaut conditionnel d’isolation applicative, pas une propriété inévitable du serverless.

Code source : exemple défectueux et modèle sûr

DANGEREUX : ÉTAT PARTAGÉ
// 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 });
}
PLUS SÛR : IDENTITÉ LOCALE
// 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 } });
}

Liste de durcissement technique

← Parcourir le répertoire complet de sécurité Explorer les guides de référence →

Références

À voir aussi PHP == et les comparaisons de magic hash Rust : le bloc unsafe est-il sûr ? strncpy() est-elle plus sûre que strcpy() ? CWE vs CVE : quelle est la différence ? Mass assignment : risques et listes de champs autorisés