- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目 - 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线 - 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径 验证: - cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1 - cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1 - git diff --check - git diff --cached --check - codegraph index . --force && codegraph status . - codegraph sync . && codegraph status .
70 lines
6.5 KiB
Markdown
70 lines
6.5 KiB
Markdown
# 7-45 ONLYOFFICE live bridge session 隔离风险
|
||
|
||
## 状态
|
||
|
||
- 状态:done
|
||
- Owner:07-ai / OnlyOffice live bridge / mnote-web
|
||
- 发现时间:2026-05-31
|
||
|
||
## 现象
|
||
|
||
ONLYOFFICE live bridge session 当前是进程内全局状态。工具调用未显式传 `onlyofficeSessionId` / `bridgeSessionId` 时,会 fallback 到最近活跃 session;同一文档的 bridge session id 又由 `docKey` 派生,重复打开同文档或多用户/多 tab 打开不同 Office 文档时,存在命令落到错误 Office tab 或 token 覆盖的风险。
|
||
|
||
## 证据
|
||
|
||
- `rust/crates/mnote-web/src/routes/onlyoffice.rs` 生成 `bridgeSessionId = "mnote-oo-" + fileState.docKey`。
|
||
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs` 的 `register_session` 按 `session_id` 覆盖 `token`、`document_id`、`asset_id` 和 `last_seen_millis`。
|
||
- `rust/crates/mnote-web/src/routes/onlyoffice_bridge.rs` 的 `current_session_info()` 直接取全局 `last_seen_millis` 最大值。
|
||
- `rust/crates/mnote-web/src/hermes_tools/onlyoffice_live.rs` 的 `resolve_session_id()` 在未传 session id 时 fallback 到 `current_session_info()`。
|
||
|
||
## 影响
|
||
|
||
- Page AI 或 Hermes tool 可能在多 Office tab 场景写错目标文档。
|
||
- 同一文档重复打开可能互相覆盖 bridge token,使旧 tab 的插件长轮询或结果回传失效。
|
||
- 多用户共享同一 mnote-web 进程时,全局最近 session 可能跨用户泄漏目标选择。
|
||
|
||
## 最小复现建议
|
||
|
||
1. 打开两个不同 Office 文档,或同一文档两个 tab。
|
||
2. 不传 `onlyofficeSessionId` 调用 `mnote.onlyoffice.session.current` 与一个写工具,例如 `mnote.onlyoffice.document.insert_text`。
|
||
3. 观察返回 session 是否只由最近活跃 tab 决定。
|
||
4. 同一文档双 tab 复测 token 覆盖后,旧 tab `commands/next` 或 `results` 是否返回 401 / 无结果。
|
||
|
||
## 修复建议
|
||
|
||
- bridge session id 加入 browser tab 级随机后缀,不只使用 `docKey`。
|
||
- Page AI run payload 必须携带当前 resource tab 的 explicit `onlyofficeSessionId`;写工具禁止默认 fallback 写入。
|
||
- `current_session` 只能作为只读诊断工具,不能作为写工具默认目标。
|
||
- session state 至少按 actor/session/document/resource 维度过滤。
|
||
|
||
## 本轮进展
|
||
|
||
- 2026-05-31:
|
||
- `onlyoffice.rs` 已把 `bridgeSessionId` 从仅基于 `docKey` 改为 `docKey + bridgeSessionSalt`,避免同文档多 tab 共用同一个 bridge session id。
|
||
- `onlyoffice_live.rs` 已禁止读/写 action fallback 到全局最近 session;读写工具必须显式传 `onlyofficeSessionId` / `bridgeSessionId`。
|
||
- `onlyoffice.rs` 的本地 callback 已绑定 bridge `sessionId` + `token`,未认证本地写回返回 401 并保持原文件不变。
|
||
- 已补 Rust 定点测试覆盖缺 explicit session 的读/写工具拒绝,以及未认证 local callback 拒绝。
|
||
- 已补 `scripts/task515-onlyoffice-live-scope-http-smoke.js` 覆盖 HTTP 层缺 explicit session 返回 400。
|
||
- 已补并通过 `scripts/task516-onlyoffice-bridge-multisession-browser-smoke.js`,在真实浏览器中验证 A/B 两个 bridge session 的 token 校验、command queue 和 result 回收互不串台,错误 token 返回 401。
|
||
- 2026-06-01:
|
||
- 已补并通过 `scripts/task518-onlyoffice-real-iframe-session-scope-smoke.js`,在真实 ONLYOFFICE iframe / DocumentServer 下打开同一 docx 两个 tab 和另一个 docx,验证同文档双 tab 共享 `docKey` 但使用不同 `bridgeSessionId`,三个插件 session 均能注册并各自回收 `selection.get` command。
|
||
- `task518` 同时覆盖显式传 B session 但 scope=A 时返回 403,以及 scope=B 授权 dry-run 返回 200,证明真实 iframe session 能被工具层按显式 session 和 resource scope 约束。
|
||
- Page AI open editors snapshot / target package 已开始保留 Office `onlyofficeSessionId` / `bridgeSessionId`,服务端 `agentTargetPackage` 与 `aiAccessScope.allowedResourceIds` 已能保留 Office asset、session 和 `resource:onlyoffice:{documentId}:{assetId}` 候选;新增 Rust 定点测试 `hermes_client_run_body_preserves_onlyoffice_target_scope` 覆盖该合同。
|
||
- ONLYOFFICE iframe 在设置 `__MNOTE_ONLYOFFICE_DEBUG__` 后会向父窗口发送 `mnote:onlyoffice-bridge-ready`,resource tab runtime 收到后事件驱动刷新 open editors snapshot;Page AI 发送前也会拒绝没有 `onlyofficeSessionId` 的 Office target,避免 bridge 未就绪时发起 run。
|
||
- `task518` 已扩展并通过非 dry-run 写入落点验证:通过 `mnote.onlyoffice.document.insert_text` 向 Office B 写入唯一 marker,再通过真实 iframe bridge `document.export` 导出 A/B 内容,断言 B 包含 marker 且 A 不包含;截图保存到 `tmp/task518-onlyoffice-real-iframe-session-scope-smoke/screenshots/office-a-after-write.png` 和 `office-b-after-write.png`。
|
||
- 已补并通过 `scripts/task523-page-ai-onlyoffice-real-target-session-smoke.js`:在真实文档页内打开 Office resource tab,等待 iframe bridge ready 后通过 Page AI target picker 选择 Office target,断言 `editorTarget` 与 `targetPackage` 均冻结真实 `onlyofficeSessionId` / `bridgeSessionId`,并断言无 `consoleErrors` / `networkFailures` / `httpErrors`。
|
||
|
||
当前 P0 安全阻断已覆盖“工具不能隐式读写全局最近 session”“callback 不能未认证写本地文件”“bridge session/token/queue 在浏览器 HTTP 层隔离”“真实 iframe 插件 session 显式授权边界”“Page AI run scope 不再被服务端压回当前页面 id”“Office bridge 未就绪时 Page AI 发送前拒绝”“真实 iframe 非 dry-run 写入不串台”和“真实 Page AI UI 从 Office iframe 目标取到 live `bridgeSessionId` 并冻结进 run payload”。
|
||
|
||
## 验收
|
||
|
||
- [x] Rust 单测覆盖页面生成同 docKey 双 session 随机 session id。
|
||
- [x] Rust 单测覆盖读/写工具缺 explicit session id 时拒绝。
|
||
- [x] Rust 单测覆盖本地 callback 缺 bridge session/token 时拒绝。
|
||
- [x] Browser smoke 覆盖两个 bridge session 的 token、command queue 与 result 隔离。
|
||
- [x] Browser smoke 覆盖两个真实 Office iframe tab 下插件注册、sessionId 隔离和 command 回收。
|
||
- [x] Rust 单测覆盖 Page AI Office target package / aiAccessScope 保留 session 与 resource scope。
|
||
- [x] 前端事件驱动刷新覆盖 ONLYOFFICE bridge ready 消息,发送前拒绝缺 session 的 Office target。
|
||
- [x] Browser smoke 覆盖两个真实 Office iframe tab 下非 dry-run 写入目标不串台。
|
||
- [x] Browser smoke 覆盖 Page AI target picker 从真实 Office iframe target 读取 live `onlyofficeSessionId` 并写入 run payload。
|