Filtración de estado serverless: mantén los datos de usuario por solicitud
Cómo los runtimes serverless reutilizados pueden exponer datos antiguos y cómo aislar la identidad y el estado de cada solicitud.
💡 Explicación sencilla (ELI5)
Trata la autenticación, la identidad del tenant y los datos de cada solicitud como una acreditación de visitante: pertenecen a una visita y no deben dejarse en una mesa compartida. Algunos runtimes serverless reutilizan un entorno o isolate para solicitudes posteriores. Si el código guarda el contexto de un usuario en estado mutable a nivel de módulo y otra solicitud lo lee, la aplicación puede cruzar su propio límite entre usuarios o tenants.
La reutilización es un comportamiento de implementación, no una garantía de que el siguiente usuario llegue al mismo runtime ni de que este siga activo durante un plazo fijo. Para que haya filtración, debe existir estado de ejecución compartido y una ruta de código que conserve datos de la solicitud o lea estado obsoleto. Mantén la identidad dentro de la solicitud y pásala explícitamente a las operaciones que la necesiten.
Conceptos principales y términos
- Execution environment reuse
- Reutilización del entorno: la plataforma puede conservar un runtime o isolate para invocaciones posteriores, aunque también puede retirarlo en cualquier momento.
- Module-level state
- Estado a nivel de módulo: valor inicializado fuera del handler. Puede permanecer en ese runtime entre invocaciones y no debe contener datos de usuario o tenant.
- AsyncLocalStorage
- AsyncLocalStorage: API de Node.js que propaga un almacén por un contexto asíncrono. Rastrea contexto; no autentica ni autoriza usuarios.
- Cross-request data exposure
- Exposición entre solicitudes: una solicitud posterior lee datos retenidos por una anterior en el mismo contexto reutilizado. La reutilización por sí sola no demuestra una filtración.
Flujo paso a paso de la exposición de datos
Una solicitud crea datos propios
La solicitud A se autentica y resuelve su contexto de usuario o tenant. El fallo aparece si el código guarda esa identidad en una variable mutable a nivel de módulo en vez de mantenerla dentro del handler.
El runtime puede reutilizarse
Tras responder, la plataforma puede mantener activo el entorno. AWS documenta la reutilización; Cloudflare Workers puede reutilizar un isolate, pero ni el enrutamiento ni su duración forman una secuencia fija entre usuarios.
Otra solicitud llega al mismo runtime
Si la solicitud B se atiende en el mismo proceso o isolate, el valor de módulo dejado por A sigue disponible hasta que se sobrescriba o limpie. Una salida anticipada o una autenticación ausente puede hacer accesible el estado antiguo.
La aplicación devuelve datos con la identidad equivocada
Si el handler usa ese valor obsoleto en una consulta o respuesta, B podría recibir datos asociados a A. Es un fallo condicional de aislamiento de la aplicación, no una propiedad inevitable de las plataformas serverless.
Código fuente: patrón defectuoso y solución segura
// 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 } });
}
Lista de endurecimiento de ingeniería
- Mantén autenticación, identidad de usuario, IDs de tenant y datos mutables de la solicitud dentro del handler u otro contexto explícitamente aislado; no los guardes en variables globales del módulo.
- Prefiere parámetros explícitos para pasar la identidad autenticada a servicios y acceso a datos. En Node.js, usa
AsyncLocalStorage.run()solo si necesitas contexto asíncrono implícito; propaga contexto, no autoriza operaciones. - No confundas un singleton del cliente de base de datos con estado de usuario. Reutiliza clientes cuando corresponda, pero pasa el usuario/tenant autenticado a cada consulta y aplica la propiedad en su condición.
- Usa
React cache()solo para la memoización por solicitud documentada en React Server Components; no como caché general de autenticación entre solicitudes. - Añade una prueba de integración que intercale solicitudes de usuarios distintos y una sin autenticar; verifica que respuestas y operaciones de base de datos se mantengan ligadas a su identidad.
- Consulta la documentación del ciclo de vida de la plataforma. Prueba la lógica suponiendo que se reutilice un entorno, sin asumir qué solicitud llegará a cada instancia.