FlawOpen
● 无服务器请求隔离

Serverless 状态泄漏:让用户数据限定在请求内

了解复用的无服务器运行时如何暴露旧用户数据,以及如何按请求隔离身份和状态。

💡 通俗解释

把身份验证、租户身份和请求数据想象成访客证:只属于一次访问,不应留在共享工作台上。某些无服务器运行时会为后续请求复用执行环境或 isolate。如果代码把用户上下文存入模块级可变状态,而后续请求读取该状态,应用就可能越过自身的用户或租户边界。

复用只是实现行为,并不保证下一位用户会进入同一运行时,也不保证运行时固定存活多久。发生泄漏需要共享执行状态,并且代码路径保留了请求数据或读取了旧状态。应让身份保持在请求范围内,并显式传递给需要它的操作。

核心概念与术语

Execution environment reuse
执行环境复用:平台可能保留 runtime 或 isolate 供后续调用使用,也可能随时回收。
Module-level state
模块级状态:在 handler 外初始化的值。它可能在该 runtime 的调用之间保留,因此不能保存用户或租户专属数据。
AsyncLocalStorage
AsyncLocalStorage:Node.js API,用于在异步执行上下文中传递存储。它跟踪上下文,不负责认证或授权。
Cross-request data exposure
跨请求数据暴露:后续请求从复用的同一上下文中读取先前请求留下的数据。仅有复用并不能证明发生泄漏。

数据暴露的逐步流程

Step 1

一个请求创建自身数据

请求 A 完成身份验证并解析自己的用户或租户上下文。如果代码把身份写入模块级可变变量,而不是保存在 handler 范围内,就会出现缺陷。

Step 2

运行时可能被复用

响应后,平台可能保留热环境。AWS 文档说明了环境复用;Cloudflare Workers 可能复用 isolate,但请求路由和存活时间并不是固定的用户间顺序。

Step 3

后续请求进入同一运行时

如果请求 B 由同一进程或 isolate 处理,A 留下的模块级值仍可读取,除非代码覆盖或清除。提前返回或缺少认证检查可能使旧状态重新可达。

Step 4

应用以错误身份返回数据

若 handler 用旧值执行数据库查询或生成响应,B 可能收到属于 A 的数据。这是有条件的应用隔离缺陷,并非无服务器平台必然会发生的情况。

源代码:缺陷模式与安全实现

不安全:共享请求状态
// 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 } });
}

工程加固清单

← 浏览完整安全目录 查看参考指南 →

参考资料

另请参阅 PHP == 与 Magic Hash 比较 Rust 的 unsafe 块仍能保证内存安全吗? strncpy() 比 strcpy() 更安全吗? CWE 与 CVE:有什么区别? Mass Assignment:风险与安全字段白名单