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 一律写「远程服务器」。
8.0 KiB
8.0 KiB
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)。
四、根因(上游机制,非配置失误)
- picker 无白名单:
@deepseek-ai/[email protected]的配置契约仅maxEntries;两个原语directoryPicker/list/createDirectory直接"从宿主文件系统作答",无 root 约束。 - 写边界 = 会话 cwd:
@deepseek-ai/dsh-sandbox-policy的Config仅mode+workspaceRoot,而workspaceRoot的 JSDoc 明确为"无 cwd 的会话 / 无 agent 调用的 fallback;正常 agent 调用使用其会话 cwd"。⇒ 把workspaceRoot钉死不能解决(只影响 fallback),必须从 bwrap 层或 picker 层收敛。 - 本部署生效值:
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 包装进程(见 §一 陷阱)。