# SECURITY — dsh-obsidian-workbench ## 威胁模型 Vault 可能包含案件资料、个人信息与未公开论文。本插件将三类数据边界显式分开: 1. **Harness 本地读取了什么**:只有用户明确授权的 Vault、被工具/UI 请求的具体文件;整库读取只在索引走查时发生,索引文件仅存本机。 2. **插件索引了什么**:~/.dsh/storages/obsidian-workbench/.db(文件路径、标题、结构、全文 FTS 副本)。纯本地,不上传。 3. **什么会发送给模型提供方**:仅在 (a) 模型调用 obsidian_read_note / get_context / search 等工具的返回内容;(b) 用户在 chips 中明确加入上下文的片段;(c) 用户在 HUD 明确点击“发送”时界面所显示的当前笔记或选区快照。注入实现:systemPrompt section 按会话渲染待注入条目,agent/pre-step 第 1 步消费队列(仅该会话、仅下一轮、消费即清空);自动同步到本机桥接层本身不会触发模型请求。 ## 防护清单(实现位置) | 威胁 | 防护 | 实现 | |---|---|---| | ../ 与绝对路径越界 | 字符串层拒绝(栈式解析 ../、拒绝绝对/盘符/NUL) | src/vault/paths.ts normalizeRelPath/joinInside | | 符号链接逃逸(读) | 逐层 realpath 校验必须落在 Vault 真实路径内 | realpathInside | | 符号链接逃逸(写) | 任何已存在前缀是符号链接(含指向内部)一律拒写,防 TOCTOU | resolveWritePath | | 编码差异(NFC/NFD) | macOS 统一 NFC 后再比较 | normalizeUnicode | | 未授权目录 | 仅设置中已确认的 Vault 出现在任何接口 | config + VaultService.vaultById | | 默认只读 | writeMode=off + allowWrite=false 双层门;门未过不读文件 | write.ts gateMode | | 未审批写入 | approval.request,非 allowed-once 一律拒绝(fail-closed) | write.ts gateApproval | | 审批期间外部修改 | 写前重读 + 内容哈希比对,不一致取消 | write.ts append/update | | 覆盖/越权覆盖 | 新建拒绝已存在目标;更新要求片段唯一匹配 | write.ts | | 非原子写入 | 同目录 tmp + rename | write.ts atomicWrite | | 误删 | 本阶段不提供删除工具;备份默认开启(.dshbak 后缀) | write.ts backupBefore | | 二进制/超大文件 | 大小上限 + NUL 嗅探 | paths.ts assertReadableFile/sniffBinary | | 恶意 HTML/脚本注入阅读器 | 渲染器只产出元素树,文本只进 text 节点(React 转义),图片仅白名单格式且经路径防护 | shared/mdrender.ts + Reader | | 附件越界 | 附件读取走 resolveReadPath + 扩展名白名单 + 20MB 上限 | service.readAttachment | | 排除目录 | .obsidian/.trash/.git/node_modules/隐藏文件/.dshbak + 用户规则 | paths.ts isExcluded | | 结果过多 | 搜索/列表/最近全部限量 | indexer/service 上限 | | 审计 | ops.jsonl 操作日志(时间/动作/路径/哈希/结果) | write.ts logOp | | 本机桥接伪造请求 | 首次启动生成 32-byte 随机令牌,文件/父目录权限 `0600`/`0700`;外部端点恒时比较 bearer;伴侣只向 HTTP loopback 发送,并校验 token 为当前用户拥有、非 symlink、严格 `0600` 的 `bridge-token` 普通文件 | hud-bridge.ts + obsidian-companion/main.js | | 伴侣伪造 Vault/path | path 拒绝绝对路径/盘符/NUL/`..`;claim 时必须匹配 confirmed Vault,再走 VaultService 路径防护 | hud-bridge.ts + web.ts + service.ts | | 过期/串会话上下文 | 活动上下文 freshness 20s;请求创建时冻结快照;按 sessionId、baseline seq、turn 关联回答 | hud-bridge.ts | | HUD 覆盖用户草稿 | running、removed、非空 draft 或其它待处理命令时不 claim;可见草稿完全一致后才 submit | InputButton.tsx | | 假取消 | 只有未领取的 queued 请求可取消;已提交模型回合不伪装为可中止 | hud-bridge.ts | | 原生选区被覆盖 | 伴侣只提交不可变选区快照;Host 建议创建与接受阶段分别复核已授权路径、文件哈希和原文唯一匹配;确认前不写盘 | obsidian-companion/main.js + web.ts + service.ts | ## 已执行的安全测试(node --test) - ../ 逃逸、绝对路径越界、盘符、NUL、空路径 - 符号链接指向 Vault 外部(读 + 写)拒绝 - Vault 内部符号链接:可读(路径落在真实目录内)、拒绝写入 - NFC/NFD 编码路径 - 不存在文件 vs 读取失败(错误码区分) - 超大文件、二进制文件 - 文件在读取窗口被删除(not_found 明确返回) - 审批期间文件被修改(冲突检测取消写入) - 未授权写入(write_disabled)、审批拒绝(approval_denied、文件未落盘) - 恶意 HTML / 脚本文本在渲染树中仅为文本节点 - 多 Vault 同名笔记:路径含 vault 维度,引用含 vault 名(linkTargetCandidates 覆盖后缀匹配;重名歧义为已知限制,见 README) - 搜索结果过量(限量断言) - HUD token 权限/鉴权、绝对与 traversal path、上下文 TTL、单会话活动请求上限、流式 turn 归属 - HTTP 层未授权 401、未确认 Vault 拒绝、选区 ContextBus 注入、回答回传 - Obsidian 伴侣 manifest、活动 Markdown 选区捕获、loopback URL、token 文件元数据、bearer header 与请求绑定 Diff 静态契约 ## 隐私边界声明 - 不启用任何遥测;除用户明确发起的模型请求外,不向第三方服务上传 Vault 内容。 - 索引与日志只写 ~/.dsh(或 DSH_HOME)与 Vault 自身(备份文件)。 - “本轮模型上下文预览”入口:输入框上方的上下文 chips 行实时显示将注入的片段、字符数、预算与截断标记;发送前可移除/清空。 - Obsidian 伴侣从代码层只允许 `http://127.0.0.1:`、`http://localhost:` 或 `http://[::1]:`;远程主机、HTTPS、userinfo、路径、查询与片段均拒绝。token 值不会通过 `/bridge/status` 返回给网页,只返回 token 文件路径。 ## 残余风险 - 索引全文副本存于本机 SQLite,未加密:与 Obsidian 自身缓存同级的本地信任假设;如需加密可后续加 SQLCipher 或按需索引(不存正文)。 - 审批 UI 依赖 Harness 的 approval 服务:若无应答渠道,写入 fail-closed(已测试)。 - 同一 Vault 内重名笔记的反链歧义:当前取全部候选匹配,可能多报;引用格式本身始终精确。 - 能读取当前 macOS 用户文件的恶意本机进程也可能读取 bridge token;这与本机 Vault/DSH credentials 的同用户信任边界一致。不要把 token 或 bridge 服务暴露到公网。 - 伴侣不直接调用 Obsidian 编辑器写入 API。AI 编辑与快速捕获只创建请求绑定的 Proposal;用户点击“确认写入”后,Harness Host 再复核请求归属、授权 Vault、规范化路径、文件哈希与原文唯一匹配,并生成备份后原子写入。