Files
MailUI4Agents/server/internal
JianFeeeee 15e4fe9203 fix(安全): 未声明 session_id 不再放行 —— 实测任意 agent 可读全部邮件正文
## 漏洞(亲自实测,不是读码推断)

用 dsh 的密钥、不带任何 session_id,逐个 GET `/api/v1/agent/mail/{id}`:

    20 封别人的信(收件方 pi / homeagent / opencode,分属
    /home/program/TrueAgent 等不同工作区)⇒ **20 封全部 200,拿到完整正文**,0 拒绝。

对照(证明闸本身没坏,只有一个缺口):

    带自己参与的 session_id 读别人的信 ⇒ 403   ← 闸有效
    不带 session_id                    ⇒ 200   ← 漏洞

根因是 `AgentMayReadSession` 的一个分支:`if scope == nil { return true }`。
该函数 2026-09-15 的注释写明「首次接线前的旧语义(未声明 scope)保持放行」——
那是**迁移期妥协**,不是设计。

## 为什么「迁移期」已经结束(实测数据推翻了当初的假设)

当初假设「未接线的桥/脚本/浏览器会走这里,等接完就收口」。而
`[agent-scope]` 警告日志累计 203 次,按调用方拆开:

    homeagent 125 / dsh 40 / pi 37 / opencode 1 / 其它 0

⇒ **202/203 来自四个桥自己**,集中在 `/api/v1/agent/mail/{id}`(pi 24 次)、
读会话参与者、`/mail/{id}/forward`。
不是「少数旧客户端没接线」,而是**主力客户端在裸奔**,而放行恰好把它们全漏过去。

日志抓手已完成使命:它精确指出了「谁还没带」,答案就是所有人。

## 为什么不能靠「补齐调用方」收口

要同时改四个桥(pi 的 `getMailSessionId` 有 `= () => ''` 的默认值,
忘注入就是静默空串 ⇒ 回到裸奔)。**默认放行与默认拒绝的差别就在这里:
前者的失败模式是沉默的。** 任何一处漏了 = 静默越权,且没有任何东西会红。

## 判据

`grep -rln AgentMayReadSession --include=*_test.go` ⇒ 修改前**零覆盖**。
一个决定安全边界的函数没有任何判据,这就是妥协能活到今天的原因。
新增 4 格:未声明须拒 / 声明且相等须放行 / 跨会话须拒 / 判定不随 agentName 变
(后者钉住 2026-09-15 裁定「每个 session 是独立『用户』」,防有人顺手加按 agent 的仲裁)。
两个变异都经得起:恢复放行、reason 改成 handler 不认识的值,各红一格。

reason 复用既有的 `not-your-session`:新增 reason 不同步改 handler 的 switch
就会把 403 变成 500;复用后 canReadSession 的 self 为空时,文案自然表达
「你还没声明自己在哪条会话」。

验证:13 包全绿 + `-race` 干净。
2026-10-02 00:34:13 +08:00
..