Skip to content

chat history saveTurn TOCTOU: lookupOwner 与 saveTurn 不在同一事务 #27

Description

@longsizhuo

背景

PR #26 引入 INV-002 阻断"匿名/登录用户往他人 chat 历史写消息"。Copilot CR 指出 controller 层的 lookupOwner + saveTurn 两步不在同一事务,存在 TOCTOU race window:

```
T1 (匿名) : lookupOwner('chat-X') → empty
T2 (alice) : saveTurn('chat-X', alice_id, ...) → INSERT INTO Chat WITH alice_id
T1 (匿名) : saveTurn('chat-X', null, ...)
→ ON CONFLICT DO UPDATE userId = COALESCE(null, alice) = alice
+ INSERT INTO Message
```

T1 的 message 进了 alice 的 chat。

影响评估

  • 利用条件极苛刻:受害者与攻击者必须毫秒级并发写同一 UUID chatId(chatId 是 `crypto.randomUUID` 生成,需 victim 主动 leak)
  • 最坏后果:一条 user message 错挂到 victim chat(不是会话接管、不是数据破坏)
  • 与 INV-002 阻止的"任意越权写入"严重程度差一个数量级,所以 PR chore(security): 用户中心 / chat / 密码 / compose 多处姿态收紧 #26 不修

推荐修法

把 `lookupOwner + saveTurn` 下沉到 service 层,包在同一 `@Transactional` 方法内,并任选其一:

方案 A(推荐):`saveTurn` SQL 加 owner WHERE 子句

```sql
-- Chat upsert 仅在 owner 兼容时命中
INSERT INTO "Chat" (id, "userId", "createdAt", "updatedAt")
VALUES (?, ?, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET
"userId" = COALESCE(EXCLUDED."userId", "Chat"."userId"),
"updatedAt" = NOW()
WHERE "Chat"."userId" IS NULL OR "Chat"."userId" = EXCLUDED."userId"
RETURNING "userId"
```

返回行数 0 → 抛 `AccessDeniedBusinessException`,message 不插入。

方案 B:service 层 `SELECT ... FOR UPDATE` 锁定 chat row 后再写
适用于已存在的 chat;新 chat 仍走插入路径。

方案 C:`@Transactional(isolation = SERIALIZABLE)` + retry
PG SSI 会在 commit 时检测 conflict 并 abort,但需要应用层重试,复杂度更高。

测试

需要新增并发测试模拟 race 场景:

```java
@test
void saveTurn防TOCTOU_并发匿名写不能污染他人chat() {
// 用 CountDownLatch 让两个 thread 在 lookupOwner 之间夹一个并发 saveTurn
// 期望:被夹在中间的"恶意匿名写"被拒,victim chat 不污染
}
```

优先级

P2(基于影响评估)。建议在下次 sprint 跟 INV-FE-001 的 CSP 加固一起做。

关联

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions