Утечка serverless-состояния: храните данные в рамках запроса
Как повторное использование serverless-среды может открыть старые данные и как изолировать идентичность и состояние каждого запроса.
💡 Простое объяснение (ELI5)
Представьте аутентификацию, идентификатор tenant и данные запроса как пропуск посетителя: он относится к одному визиту и не должен оставаться на общем рабочем столе. Некоторые serverless-среды повторно используют среду выполнения или isolate для следующих запросов. Если код сохраняет пользовательский контекст в изменяемой переменной уровня модуля, а другой запрос читает её, приложение может нарушить собственную границу между пользователями или tenant.
Повторное использование — особенность реализации, а не гарантия, что следующий пользователь попадёт в ту же среду или что она останется активной фиксированное время. Для утечки нужны общий контекст выполнения и путь в коде, который сохраняет данные запроса или читает устаревшее состояние. Храните идентичность в пределах запроса и явно передавайте её нужным операциям.
Основные понятия и термины
- Execution environment reuse
- Повторное использование среды: платформа может сохранить runtime или isolate для следующих вызовов, но может завершить его в любой момент.
- Module-level state
- Состояние уровня модуля: значение, инициализированное вне обработчика. Оно может сохраниться в этой среде между вызовами и не должно содержать пользовательские или tenant-данные.
- AsyncLocalStorage
- AsyncLocalStorage: API Node.js для распространения хранилища в асинхронном контексте. Он отслеживает контекст, но не аутентифицирует и не авторизует пользователя.
- Cross-request data exposure
- Межзапросное раскрытие данных: последующий запрос читает данные предыдущего в той же повторно используемой среде. Само повторное использование не доказывает утечку.
Пошаговая цепочка раскрытия данных
Запрос создает собственные данные
Запрос A проходит аутентификацию и получает контекст пользователя или tenant. Ошибка возникает, если код записывает эту идентичность в изменяемую переменную уровня модуля, а не хранит в обработчике.
Среда может быть использована повторно
После ответа платформа может сохранить среду активной. AWS описывает повторное использование; Cloudflare Workers может использовать isolate повторно, но маршрутизация и срок жизни не образуют фиксированную последовательность между пользователями.
Следующий запрос попадает в ту же среду
Если запрос B обслуживается тем же процессом или isolate, значение уровня модуля от A остается доступным до перезаписи или очистки. Ранний выход или пропущенная аутентификация могут открыть доступ к старому состоянию.
Приложение возвращает данные с чужой идентичностью
Если обработчик использует устаревшее значение в запросе к БД или ответе, B может получить данные A. Это условная ошибка изоляции приложения, а не неизбежное свойство serverless-платформ.
Исходный код: опасный шаблон и безопасный вариант
// 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 } });
}
Контрольный список усиления защиты
- Храните аутентификацию, идентификатор пользователя, tenant ID и изменяемые данные запроса в обработчике или явно изолированном контексте; не помещайте их в глобальные переменные модуля.
- Предпочитайте явные параметры при передаче аутентифицированной идентичности сервисам и доступу к данным. В Node.js применяйте
AsyncLocalStorage.run()только для неявного асинхронного контекста; он передает контекст, но не авторизует операции. - Не считайте singleton клиента БД пользовательским состоянием. Клиенты можно переиспользовать, но передавайте аутентифицированного пользователя/tenant в каждый запрос и проверяйте владельца в условии запроса.
- Используйте
React cache()только для документированной межкомпонентной мемоизации в рамках запроса React Server Components; не как общий межзапросный кэш аутентификации. - Добавьте интеграционный тест с чередованием запросов разных пользователей и неаутентифицированного запроса; проверьте привязку каждого ответа и операции БД к своей идентичности.
- Изучите документацию жизненного цикла платформы. Проверяйте логику на случай повторного использования среды, не предполагая, какой запрос попадет в какой экземпляр.