●
无服务器请求隔离
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 } });
}
工程加固清单
- 将认证信息、用户身份、租户 ID 和请求专属可变数据保存在 handler 或明确的请求级上下文中;不要放在模块全局变量里。
- 优先使用显式参数把已认证身份传给服务和数据访问函数。Node.js 中只有在确需隐式异步上下文时才使用
AsyncLocalStorage.run();它传递上下文,不负责授权。 - 不要把数据库客户端 singleton 当作用户状态。可按需复用客户端,但每次查询都要传入已认证的用户/租户,并在查询条件中执行所有权检查。
- 仅将
React cache()用于 React Server Components 文档说明的请求级记忆化;不要将其用作通用跨请求认证缓存。 - 添加集成测试,交错执行不同用户及未认证请求,并确认每个响应和数据库操作都绑定到各自的身份。
- 查阅平台生命周期文档。按执行环境可能复用来测试处理逻辑,但不要假定具体请求会被调度到哪一个实例。