Files
dsh_shenxian/dsh-server-docs/04-调整方案/17-工作区选择器暴露面核查.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

91 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 17 · 工作区选择器暴露面核查("选择工作区目录"为何能看到 `/`)
- 日期:2026-09-10
- 触发:用户以 admin 登录 dsh 会话后点「添加工作区」→ 目录选择器显示 `/`(`dev`/`etc`/`lib64`/`proc`/`tmp`/`usr`/`var`),询问是否有问题
- 结论一句话:**不是越权漏洞**(跨租户与系统目录的读/写均被拦住),但确有两个真问题 —— ① 工作区选择器**无根目录白名单**;② 沙箱写边界 = **会话 cwd**,用户把自家目录加为工作区后即可写自家 profile(含平台管理的 `cordis.patch.yml`)。次要项:`/etc/nftables-dsh-egress.nft` 为 644 可读。
- 状态:核查完成 → 收敛项 H1-H4 待确认后执行
> **TL;DR**|**结论**:「添加工作区」为何能看到 `/`:**不是越权漏洞**(跨租户与系统目录读/写均被拦),但确有两个真问题。
> **关键**:① picker **无根目录白名单**;② 沙箱写边界 = **会话 cwd**(用户把自家目录加为工作区后可改平台策略文件)→ 分别由档案 18 与写保护 B1 处置。
> **状态**:🔍 核查完成(结论已落地)
---
## 一、现象与机制还原(源码 + 进程实测)
截图里的 7 项**不是宿主根**,而是 bwrap **合成根**。实际 spawn 命令行(`pgrep -f "dsh --profile web"` 实测):
```
bwrap --ro-bind /usr /usr --ro-bind /lib64 /lib64 --ro-bind /etc /etc
--dev /dev --proc /proc
--bind <userRoot>/tmp /tmp
--bind <userRoot> <userRoot>
--unshare-pid --chdir <userRoot>/ws
-- setpriv --reuid 114801 --regid 114801 --clear-groups \
dsh --profile web --host 127.0.0.1 --port 36699
```
bwrap 新建 mount namespace 并从**空根**合成:只 bind 上述路径,`--unshare-pid` 隔离进程视角。`/var` 等中间目录是为 bind 目标自动补的父目录 → 因此 `/` 下只见 7 项(`dev etc lib64 proc tmp usr var`),**不含宿主的 `/opt` `/root` `/home` `/www`**。截图与实测完全一致 ⇒ 没有额外泄漏。
> ⚠️ 一个诊断陷阱:`pgrep -f "dsh --profile web"` 会先命中 bwrap 包装进程(root),在其 namespace 内以 uid 0 探测会得出"全部可读写"的**假象**。必须取 `ps -eo user,args` 中 user 为 `dsh-*` 的那个子进程。
## 二、实测结果
以实例 uid `114801`(= admin `cce6d1cd-…`)的 mount/pid namespace 只读探测:
| 类别 | 项 | 结果 |
|---|---|---|
| **敏感文件可读** | `/etc/passwd` | ⚠️ **READABLE** —— 泄露 `admin`、`www`、`dsh-cce6d1cdb376430480f0`、**`dsh-eeccbc638afc46bdb663`(另一租户账号名,含 UUID 前 20 位)** 及各自 home 路径 |
| | `/etc/shadow` | ✅ 不可读 |
| | `/etc/dshs.env` | ✅ 不可读(600 有效) |
| | `/etc/nginx/nginx.conf`、`/etc/ssh/sshd_config` | ✅ 不可读 |
| | `/etc/nftables-dsh-egress.nft` | ⚠️ **可读(644)** —— 泄露出网护栏策略(无凭据) |
| **写权限**(`test -w`) | `/`、`/etc`、`/usr`、`/var`、`/opt` | ✅ 全 ro(bwrap ro-bind 硬保护,内核级) |
| | `/tmp` | ✅ 可写,但为**每用户私有**(`<userRoot>/tmp` bind;`dsh-spill-*`、`node-addon-*` 均属本 uid) |
| | `<userRoot>`(自家根,rw bind) | ⚠️ **可写** —— 含 `home/profiles/web/*`、`patches/`、`settings.yaml` |
| **跨租户** | `/opt`、`/opt/dsh`、`/opt/dshs`、`/root`、`/home`、`/home/admin` | ✅ 全部 denied |
| | `/var/lib/dshs/users/*` 他人目录 | ✅ 不可见(700 + 非本人) |
| **进程视角** | `/proc` | ✅ 仅见自身(`--unshare-pid`) |
## 三、风险判定
| 级别 | 问题 | 说明 |
|---|---|---|
| **P1** | **工作区写边界可被用户自行扩大** | `dsh-sandbox-policy` 的写边界 = **会话 cwd**;用户用「添加工作区」把 `<userRoot>` 或 `home/profiles/web` 加为工作区后,agent 即可写自家 profile:改 `cordis.patch.yml` → **绕过 `ensure-role-profile-patch.cjs` 的角色裁剪**(普通用户可自行恢复「模型设置」分区)、改 `settings.yaml` / patch 集合 → 破坏平台对 profile 的管理语义。属**租户内自我提权/策略绕过**,不跨租户 |
| **P2** | 工作区选择器无根白名单 | 上游 `dsh-host-directory-picker-browse` 配置**只有 `maxEntries`(默认 1000),无根目录字段** → 用户可浏览并选择 `/usr`、`/etc`、`/tmp`、自家根。因 `/usr`/`/etc` 为 ro-bind 故**写不进去**,但会扩大**只读**暴露面(读 `/etc/passwd`、`/usr` 下 dsh 源码与 npm 包) |
| **P2** | `/etc/passwd` 泄露另一租户账号名 | 账号名含对端 UUID 前 20 hex;home 路径一并泄露(`/home` 亦曾可列,见档案 14)。不足以直接越权(目录 700),但属租户 ID 泄露,配合门户 `/u/<id>/dsh/` 路由可做探测 |
| **P3** | `nftables-dsh-egress.nft` 644 可读 | 仅泄露"封了元数据端点 + 观测外联"这一策略事实,无凭据 |
**结论**:隔离本体有效(跨租户 + 系统目录读写均被拦),截图不是漏洞;真正要收敛的是 **P1(写边界)+ P2(选择器范围 / passwd)**。
## 四、根因(上游机制,非配置失误)
1. **picker 无白名单**:`@deepseek-ai/[email protected]` 的配置契约仅 `maxEntries`;两个原语 `directoryPicker/list` / `createDirectory` 直接"从宿主文件系统作答",无 root 约束。
2. **写边界 = 会话 cwd**:`@deepseek-ai/dsh-sandbox-policy` 的 `Config` 仅 `mode` + `workspaceRoot`,而 `workspaceRoot` 的 JSDoc 明确为"**无 cwd 的会话 / 无 agent 调用的 fallback**;正常 agent 调用使用其**会话 cwd**"。⇒ **把 `workspaceRoot` 钉死不能解决**(只影响 fallback),必须从 **bwrap 层**或 **picker 层**收敛。
3. 本部署生效值:`mode: workspace-write`(`--dump-config` 实测:`process.env.DSH_PERMISSION_MODE ?? 'workspace-write'`,环境未设该变量),`approval: ask`。
## 五、收敛建议
| # | 措施 | 收益 | 代价 / 风险 | 建议 |
|---|---|---|---|---|
| **H1** | `chmod 600 /etc/nftables-dsh-egress.nft` | 消除 P3 策略泄露 | 极低(1 行,可逆) | ✅ **立即执行** |
| **H2** | **bwrap 精修**:对平台管理物加 `--ro-bind` **覆盖**(bwrap 后 bind 覆盖先 bind)——如 `--ro-bind <userRoot>/patches <userRoot>/patches`、角色 patch 文件 | 即使 cwd 落在自家目录内,平台管理文件也**写不动**(内核级) | 中:需先验证 dsh 是否会重写这些文件(若 `ensure-role-profile-patch.cjs` 在 spawn 前由 root 重写,则 ro 覆盖可行;若需实例自身写则需改为"启动期 `--patch` 注入角色补丁"替代 profile 文件) | ✅ 推荐,需先做一次写行为验证 |
| **H3** | **picker 白名单**:自建 host 插件覆盖 `directoryPicker/list` seam,根固定为 `<userRoot>/ws` | 从源头消除 P2(连读也不给) | 中高:新增 host 插件(参考 `poc/business-plugins` 先例);需验证 seam 覆盖优先级 | 🔄 v2 评估 |
| **H4** | `/etc/passwd` 账号名**去 UUID 化**(uid 分配改顺序号,账号名不含用户 ID) | 收敛 P2 租户 ID 泄露 | 中:改动 uid 分配 + 存量账号迁移 | 🔄 可选,视租户规模 |
| **H5** | 若接受现状:把"工作区选择器可见沙箱根 + 写边界随会话 cwd"记为**已知边界**(同档案 14 风格) | 0 成本 | — | 兜底选项 |
## 六、回滚
| 项 | 回滚 |
|---|---|
| H1 | `chmod 644 /etc/nftables-dsh-egress.nft` |
| H2 | 移除 orchestrator 中新增的 `--ro-bind` 参数并重启实例 |
| H3 | 卸载自建插件(profile bundles 回退) |
| H4 | 恢复原 uid 分配逻辑(存量账号不受影响) |
## 七、红线遵守
- R1:不触发 dsh 升级;所有建议基于当前 `0.1.2-rc.1` 既有机制。
- R2:**不改官方 dsh 主程序与缓存** —— H2 改的是编排器 spawn 参数,H3 走 profile 层插件,均不触碰 `@deepseek-ai/dsh`。
- R3:本核查**全程只读**(仅 `test -r`/`test -w`/`ls -ld`,未创建或修改任何文件)。
- 截图与结论同源可复现;诊断脚本须避开 bwrap 包装进程(见 §一 陷阱)。