背景
PR #129(多用户登录)修了 chat 会话读(listChatMessages)和改标题(setChatThreadTitle)两处越权,靠 apps/dashboard/src/lib/mastra.ts 的 ownsThread(取回 thread 校验 resourceId === 登录用户)。但聊天发送/续接主路径没有同源校验。
问题
/api/copilotkit(apps/dashboard/src/app/api/copilotkit/route.ts)把 resourceId 强制设为 getSessionSubject()(可信),但 threadId 来自前端 <CopilotKit threadId>(客户端 crypto.randomUUID() 生成、存 localStorage,客户端可控)。
- mastra 侧
packages/orchestration/src/mastra/memory.ts 的 assertScopedRequest() 只校验 resourceId/threadId 非空,不比对已存在 thread 的 resourceId 是否等于调用方;且该函数目前只在 scheduler/runner.ts 调,聊天路径没调。
identityMiddleware(mastra/index.ts)注释明确写它与 Memory 的 resourceId 机制"完全解耦",只管 AUTH_SUB_KEY 给 askCache 用,不 reconcile 请求体里的 resourceId/threadId。
影响(潜在,非当前可利用)
登录用户 B 若拿到用户 A 的 threadId,向 /api/copilotkit 发消息传 threadId=A的thread → mastra 侧无环节校验该 thread 归属 → B 的消息写进 A 的会话、B 本轮还可能读到 A 的历史上下文;A 下次打开该会话看到 B 注入的消息。
当前不可利用:threadId 是不可枚举的 UUIDv4,从不展示给其他用户、不进 URL、无"分享会话"功能 → 今天没有获取他人 threadId 的向量。属防御纵深缺口。
触发条件(务必在此之前修)
加任何会让 threadId 泄露的功能之前必须先修:分享会话 / 导出会话 / 把 threadId 放进 URL / 客服可见的会话链接 等。
建议修法
服务端(正确层次):在 mastra agent.stream/generate 前(或 identityMiddleware / 一个 input processor 里)对已存在的 threadId 加一次 resourceId 归属校验,复用 PR #129 ownsThread 思路——thread 存在且 resourceId !== 已认证 sub → 拒绝;不存在(新会话)或本人 → 放行。需先读 @mastra/memory 内部确认拦截点(PR #129 review 因沙箱无 node_modules 未能二次交叉确认,置信度高但非 100%)。
备选:dashboard BFF 在 /api/copilotkit 解析 threadId 做同样 3 态校验(新建/本人/他人),但要解析并重建 CopilotKit 请求体流,较 fragile。
来源
PR #129 第 5 轮 auto-review。同根问题的兄弟修复已在该 PR 落地(读/改标题两处)。
背景
PR #129(多用户登录)修了 chat 会话读(
listChatMessages)和改标题(setChatThreadTitle)两处越权,靠apps/dashboard/src/lib/mastra.ts的ownsThread(取回 thread 校验resourceId === 登录用户)。但聊天发送/续接主路径没有同源校验。问题
/api/copilotkit(apps/dashboard/src/app/api/copilotkit/route.ts)把resourceId强制设为getSessionSubject()(可信),但threadId来自前端<CopilotKit threadId>(客户端crypto.randomUUID()生成、存 localStorage,客户端可控)。packages/orchestration/src/mastra/memory.ts的assertScopedRequest()只校验resourceId/threadId非空,不比对已存在 thread 的resourceId是否等于调用方;且该函数目前只在scheduler/runner.ts调,聊天路径没调。identityMiddleware(mastra/index.ts)注释明确写它与 Memory 的resourceId机制"完全解耦",只管AUTH_SUB_KEY给 askCache 用,不 reconcile 请求体里的resourceId/threadId。影响(潜在,非当前可利用)
登录用户 B 若拿到用户 A 的
threadId,向/api/copilotkit发消息传threadId=A的thread→ mastra 侧无环节校验该 thread 归属 → B 的消息写进 A 的会话、B 本轮还可能读到 A 的历史上下文;A 下次打开该会话看到 B 注入的消息。当前不可利用:
threadId是不可枚举的 UUIDv4,从不展示给其他用户、不进 URL、无"分享会话"功能 → 今天没有获取他人 threadId 的向量。属防御纵深缺口。触发条件(务必在此之前修)
加任何会让
threadId泄露的功能之前必须先修:分享会话 / 导出会话 / 把 threadId 放进 URL / 客服可见的会话链接 等。建议修法
服务端(正确层次):在 mastra
agent.stream/generate前(或identityMiddleware/ 一个 input processor 里)对已存在的threadId加一次resourceId归属校验,复用 PR #129ownsThread思路——thread 存在且resourceId !== 已认证 sub→ 拒绝;不存在(新会话)或本人 → 放行。需先读@mastra/memory内部确认拦截点(PR #129 review 因沙箱无 node_modules 未能二次交叉确认,置信度高但非 100%)。备选:dashboard BFF 在
/api/copilotkit解析 threadId 做同样 3 态校验(新建/本人/他人),但要解析并重建 CopilotKit 请求体流,较 fragile。来源
PR #129 第 5 轮 auto-review。同根问题的兄弟修复已在该 PR 落地(读/改标题两处)。