Vazamento de estado serverless: mantenha dados de usuários no escopo da requisição
Veja como runtimes serverless reutilizados podem expor dados antigos de usuários e como isolar identidade e estado por requisição.
💡 Explicação em linguagem simples (ELI5)
Trate autenticação, identidade do tenant e dados específicos da requisição como um crachá de visitante: pertencem a uma visita e não devem ficar numa bancada compartilhada. Alguns runtimes serverless reutilizam um ambiente de execução ou isolate em requisições posteriores. Se o código guardar o contexto de um usuário em estado mutável no escopo do módulo e outra requisição o ler, a aplicação pode cruzar a própria fronteira entre usuários ou tenants.
A reutilização é um comportamento de implementação, não uma garantia de que o próximo usuário chegará ao mesmo runtime nem de que ele ficará ativo por um período fixo. O vazamento exige estado de execução compartilhado e um caminho de código que preserve dados da requisição ou leia estado antigo. Mantenha a identidade local à requisição e passe-a explicitamente às operações que precisam dela.
Conceitos principais e termos
- Execution environment reuse
- Reutilização do ambiente de execução: a plataforma pode manter um runtime ou isolate disponível para invocações posteriores, mas também pode encerrá-lo a qualquer momento.
- Module-level state
- Estado no escopo do módulo: valor inicializado fora do handler. Pode permanecer no runtime entre invocações e não deve guardar dados específicos de usuário ou tenant.
- AsyncLocalStorage
- AsyncLocalStorage: API do Node.js para propagar um armazenamento por um contexto assíncrono. Ela rastreia contexto; não autentica nem autoriza usuários.
- Cross-request data exposure
- Exposição de dados entre requisições: falha em que uma requisição posterior lê dados mantidos de uma anterior no mesmo contexto reutilizado. A reutilização, sozinha, não prova vazamento.
Fluxo passo a passo da exposição de dados
Uma requisição cria dados próprios
A Requisição A autentica e resolve seu contexto de usuário ou tenant. O bug aparece se o código grava essa identidade numa variável mutável no escopo do módulo, em vez de mantê-la no handler.
O runtime pode ser reutilizado
Depois da resposta, a plataforma pode manter o ambiente aquecido. A AWS documenta reutilização de ambientes; o Cloudflare Workers pode reutilizar um isolate, mas o roteamento e a vida útil não formam uma sequência fixa entre usuários.
Uma requisição posterior chega ao mesmo runtime
Se a Requisição B for atendida pelo mesmo processo ou isolate, um valor no escopo do módulo deixado pela Requisição A continua disponível até ser sobrescrito ou limpo. Um retorno antecipado ou uma falha de autenticação pode tornar o estado antigo acessível.
A aplicação retorna dados com a identidade errada
Se o handler usar esse valor antigo numa consulta ou resposta, B pode receber dados associados a A. É uma falha condicional de isolamento da aplicação, não uma propriedade inevitável das plataformas serverless.
Código-fonte: padrão defeituoso e implementação 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 } });
}
Checklist de hardening de engenharia
- Mantenha autenticação, identidade do usuário, IDs de tenant e dados mutáveis da requisição no handler ou em outro contexto explicitamente isolado; não os armazene em variáveis globais do módulo.
- Prefira parâmetros explícitos para passar a identidade autenticada às funções de serviço e acesso a dados. No Node.js, use
AsyncLocalStorage.run()somente quando precisar de contexto assíncrono implícito; isso propaga contexto, não autoriza operações. - Não trate um singleton de cliente de banco de dados como estado do usuário. Reutilize clientes quando apropriado, mas passe o tenant/usuário autenticado a cada consulta e imponha a propriedade na condição da consulta.
- Use
React cache()somente para a memoização por requisição documentada em React Server Components; não o use como cache geral de autenticação entre requisições. - Adicione um teste de integração que intercale requisições de usuários diferentes e uma requisição sem autenticação; confirme que resposta e operação no banco permanecem ligadas à própria identidade.
- Consulte a documentação do ciclo de vida da plataforma. Teste a lógica supondo que um ambiente possa ser reutilizado, sem presumir qual requisição será enviada a qual instância.