FlawOpen
● Serverless リクエスト分離

Serverless の状態漏えい:ユーザーデータをリクエストに限定

再利用される serverless runtime が古いユーザーデータを露出する条件と、ID・状態をリクエスト単位で分離する方法を説明します。

💡 やさしい説明

認証情報、tenant ID、リクエスト固有データは来訪者バッジのようなものです。一度の訪問に属し、共有の作業台に置き忘れてはいけません。一部の serverless runtime は後続リクエストで実行環境や isolate を再利用します。モジュールスコープの可変状態にユーザーコンテキストを保存し、別のリクエストが読むと、アプリ自身のユーザー・tenant 境界を越えるおそれがあります。

再利用は実装上起こり得る動作であり、次のユーザーが同じ runtime に到達する保証でも、一定時間稼働する保証でもありません。漏えいには、共有実行状態とリクエストデータを残す、または古い状態を読むコード経路の両方が必要です。ユーザー ID はリクエスト内に保持し、必要な処理へ明示的に渡してください。

主な概念と用語

Execution environment reuse
実行環境の再利用:プラットフォームは後続の呼び出しに備えて runtime や isolate を維持する場合がありますが、いつでも破棄できます。
Module-level state
モジュールスコープ状態:handler の外で初期化された値。同じ runtime 内の呼び出し間で残る場合があるため、ユーザー固有データを保持してはいけません。
AsyncLocalStorage
AsyncLocalStorage:非同期実行コンテキスト内で store を伝播する Node.js API。コンテキストを追跡するもので、ユーザーを認証・認可する機能ではありません。
Cross-request data exposure
リクエスト間のデータ露出:再利用コンテキストで後続リクエストが前のデータを読む失敗。再利用だけでは漏えいの証明になりません。

データ露出の流れ

Step 1

最初のリクエストが固有データを作る

リクエスト A は認証し、自身のユーザーまたは tenant コンテキストを解決します。handler 内に保持せず、モジュールスコープの可変変数へ書き込むと問題になります。

Step 2

runtime が再利用される場合がある

応答後もプラットフォームが環境を維持することがあります。AWS は環境再利用を説明しています。Cloudflare Workers も isolate を再利用する場合がありますが、ユーザー間のルーティング順や寿命は固定されません。

Step 3

後続リクエストが同じ runtime に届く

リクエスト B が同じプロセスまたは isolate で処理されると、A が残したモジュール値は上書き・消去されるまで利用可能です。早期 return や認証漏れによって古い状態を参照する場合があります。

Step 4

誤ったユーザー ID でデータを返す

handler が古い値で DB を検索したり応答を作ったりすると、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 });
}
より安全:リクエスト内の ID
// 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 } });
}

エンジニアリング強化チェックリスト

← セキュリティディレクトリ全体を見る リファレンスガイドを見る →

参考資料

関連項目 PHP == と Magic Hash の比較 Rust の unsafe ブロックはメモリ安全なのか? strncpy() は strcpy() より安全ですか? CWE と CVE:何が違うのか Mass Assignment:リスクと安全なフィールド許可リスト