Files
MailUI4Agents/server/internal/repo
JianFeeeee 095213b981 fix(安全)★★: 声明别人的 session_id 就能读那封信 —— 补「参与方」判据
## 漏洞(实测,生产可利用)

对照实验(同一会话、同一 Agent,绕开 MCP 直打原生端点):

    主人 mcp-peer 读自己的信(带正确 session_id)        ⇒ 200(正常)
    他人 mcp-probe 读同一封信(**带正确 session_id**)      ⇒ 200 ★ 泄露正文

绕开 MCP、直接 `GET /api/v1/agent/mail/{id}?session_id=...` 同样 200
⇒ 根因在网关,不在任何接入方式。

生产复核(现行 8180,未改任何代码):

    dsh 声明 gui-lab 的 session_id ⇒ HTTP 200,拿到完整正文

## 根因:判据里没有「你是谁」这一项

上一版(今天早些时候,15e4fe9 那次)只把 `if scope == nil { return true }`
改成拒绝,堵住的是「**不声明** session_id 就放行」。剩下的半边是
「声明一个**别人的** session_id」。

判据只有一句 `*scope != target` —— 它问的是「你声明的会话是不是目标会话」,
而 `session_id` **由请求方自己给**。于是任何持有 Agent 凭据的客户端只要报出
一个已存在的会话 id,就能以那条会话的身份读它。

漏洞的形状就写在签名里:函数**收了** `agentName`,却被 `_ = agentName` 丢弃。

## 「信任边界在桥」这个前提不成立

函数头原来写着「信任边界在**桥**:`session_id` 由 worker 闭包注入(模型改不了)」。
那是对我们自家四个桥的陈述,**不是**服务端能强制的事实:

1. 桥与网关之间是普通 HTTP。任何拿到 Agent 凭据的客户端都能直接调这些端点
   (本次实测即是如此)。
2. 「由闭包注入」是对我们自己代码的信心,不是收到请求时能重新验证的事实。

上一版收口时也用过同类理由(「迁移期未结束」),实测同样不成立 ——
这是同一天内第二次。

## 修法:把「声明」变成可验证的事实

保留 `*scope != target`(2026-09-15 裁定:会话是独立单位、不跨会话读取),
**另加**一道参与方校验:

    scope == target 且 agentName ∈ 该会话的参与方(from / to / cc) ⇒ 放行

- 不引入工作区轴(那是 cwd/沙箱那条轴,2026-09-15 明确划开)。
- 不新建表、不加迁移。
- 参与方判定与 `SessionParticipants` 同源(逐封扫 mails,同一个 `models.Address`
  JSON 解析),避免两处对同一份数据给出不同答案。
- 抄送方算参与方:生产里有 2622 行非空 cc_list,只认 from/to 会把正当读者判成外人。

## fail closed

查参与方出错(DB 不可用、cc_list 解析失败)一律拒绝并带错误。
这道闸的失败模式必须是沉默的拒绝 —— 一旦「查不到就放行」,
数据库一抖就等于把漏洞重新打开,且没有任何日志。

## 判据(9 格,含改写)

改写 2 格:`AllowsOwnSession` 原来用 `uuid.New()` 造一条**不存在的**会话
就断言放行 —— 那正是漏洞的形状;`IgnoresAgentName` 断言「身份不参与判断」,
**这条断言本身就是漏洞**。两者都改成断言新事实。

新增 7 格,覆盖:声明别人会话被拒 / 抄送方放行 / 主收件方放行 / 空身份拒绝 /
判定随身份改变 / 跨会话仍拒(参与方也不行)/ fail closed。

**变异验证**(每条确认已应用后才数红格):

    退回漏洞原状(跳过参与方校验)   → 红 3
    只认 from_agent(漏 to 与 cc)  → 红 1 ★(先测时红格为 0,补了主收件方那格才抓住)
    fail open(查不到就放行)        → 红 1 ★(同样先红格为 0,补了 fail-closed 那格)

后两条是**补判据的过程**:`return true` 那版和「只看 from」那版都能全套通过,
说明原先的判据盯不住这两个改法。

## 对现有桥的影响(部署前实测)

按生产数据核对四个桥:「from_agent 是它、但它不是任何邮件参与方」的会话
只有 1 条,且**零邮件**(一条权限请求测试会话)—— 那类会话没有邮件可读,
判据影响为零。桥不会被误伤。

## 波及面

`AgentMayReadSession` 有 5 个消费点:read_mail / read_thread / 读会话参与者 /
forward 的源信 / AgentGetMailThread 另一分支。全部自动获得这道判据。

全量 14 包绿。
2026-10-02 13:27:43 +08:00
..