Serverless State Bleed: Benutzerdaten an Anfragen binden
Wie wiederverwendete Serverless-Runtimes alte Benutzerdaten offenlegen können und wie Identität und Zustand je Anfrage isoliert werden.
💡 Einfach erklärt (ELI5)
Behandeln Sie Authentifizierung, Tenant-Identität und anfragespezifische Daten wie einen Besucherausweis: Er gehört zu einem Besuch und darf nicht auf einer gemeinsam genutzten Werkbank liegen bleiben. Manche Serverless-Runtimes verwenden eine Ausführungsumgebung oder ein Isolate für spätere Anfragen erneut. Speichert der Code den Benutzerkontext als veränderlichen Modulzustand und liest eine andere Anfrage ihn aus, kann die Anwendung ihre eigene Grenze zwischen Benutzern oder Tenants überschreiten.
Wiederverwendung ist ein Implementierungsverhalten, keine Garantie, dass der nächste Benutzer dieselbe Runtime erreicht oder sie eine feste Zeit lang aktiv bleibt. Ein Leck setzt gemeinsam genutzten Ausführungszustand und einen Codepfad voraus, der Anfragedaten weiterträgt oder veralteten Zustand liest. Halten Sie Identität anfragebezogen und übergeben Sie sie explizit an benötigte Vorgänge.
Kernbegriffe
- Execution environment reuse
- Wiederverwendung der Ausführungsumgebung: Die Plattform kann eine Runtime oder ein Isolate für spätere Aufrufe bereithalten, sie aber jederzeit beenden.
- Module-level state
- Modulzustand: außerhalb des Handlers initialisierter Wert. Er kann in dieser Runtime zwischen Aufrufen bestehen bleiben und darf keine benutzer- oder tenantbezogenen Daten enthalten.
- AsyncLocalStorage
- AsyncLocalStorage: Node.js-API zur Weitergabe eines Stores innerhalb eines asynchronen Kontexts. Sie verfolgt Kontext, authentifiziert oder autorisiert aber niemanden.
- Cross-request data exposure
- Datenoffenlegung zwischen Anfragen: Eine spätere Anfrage liest Daten aus einer früheren im selben wiederverwendeten Kontext. Wiederverwendung allein beweist kein Leck.
Ablauf einer Datenoffenlegung
Eine Anfrage erzeugt eigenen Zustand
Anfrage A authentifiziert sich und ermittelt ihren Benutzer- oder Tenantkontext. Der Fehler entsteht, wenn der Code die Identität in eine veränderliche Modulvariable schreibt statt sie im Handler zu halten.
Die Runtime kann wiederverwendet werden
Nach der Antwort kann die Plattform die Umgebung warm halten. AWS dokumentiert die Wiederverwendung; Cloudflare Workers kann ein Isolate wiederverwenden, aber Routing und Lebensdauer bilden keine feste Abfolge zwischen Benutzern.
Eine spätere Anfrage erreicht dieselbe Runtime
Wird Anfrage B im selben Prozess oder Isolate verarbeitet, bleibt der Modulwert von A verfügbar, bis er überschrieben oder gelöscht wird. Ein vorzeitiger Rücksprung oder fehlende Authentifizierung kann den alten Zustand erreichbar machen.
Die Anwendung liefert Daten unter falscher Identität
Nutzt der Handler den alten Wert für eine Datenbankabfrage oder Antwort, kann B Daten von A erhalten. Das ist ein bedingter Anwendungsfehler bei der Isolation, keine unvermeidliche Eigenschaft von Serverless-Plattformen.
Quellcode: unsicheres Muster und sichere Umsetzung
// 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 });
}
// 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 } });
}
Checkliste zur technischen Härtung
- Halten Sie Authentifizierung, Benutzeridentität, Tenant-IDs und veränderliche Anfragedaten im Handler oder einem ausdrücklich anfragebezogenen Kontext; speichern Sie sie nicht in globalen Modulvariablen.
- Bevorzugen Sie explizite Parameter für die Übergabe der authentifizierten Identität an Service- und Datenzugriffsfunktionen. Nutzen Sie
AsyncLocalStorage.run()in Node.js nur bei Bedarf für impliziten asynchronen Kontext; es propagiert Kontext, autorisiert aber keine Vorgänge. - Verwechseln Sie einen Datenbank-Client-Singleton nicht mit Benutzerzustand. Clients können wiederverwendet werden, aber übergeben Sie jeder Abfrage den authentifizierten Benutzer/Tenant und erzwingen Sie Eigentümerschaft im Prädikat.
- Verwenden Sie
React cache()nur für die dokumentierte anfragebezogene Memoisierung in React Server Components, nicht als allgemeinen anfrageübergreifenden Authentifizierungscache. - Ergänzen Sie einen Integrationstest mit verschachtelten Anfragen verschiedener Benutzer und einer nicht authentifizierten Anfrage. Prüfen Sie, dass Antwort und Datenbankvorgang an die jeweilige Identität gebunden bleiben.
- Beachten Sie die Lebenszyklusdokumentation der Plattform. Testen Sie so, als könne eine Umgebung wiederverwendet werden, ohne anzunehmen, welche Anfrage welche Instanz erhält.