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

8.0 KiB
Raw Blame History

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 包装进程(见 §一 陷阱)。