DeepSeek Harness (dsh) 安全审计报告

53次阅读
没有评论

DeepSeek Harness (dsh) 安全审计报告

  • 目标:/11-code/deepseek-harness(@deepseek-ai/dsh-root v0.1.0-rc.5,MIT,上游 github.com/deepseek-ai/deepseek-harness,HEAD 47f943859b)
  • 方式:源码级静态审计。8 个攻击面域并行深度审查(沙箱隔离 / 文件系统 / Web 宿主 / 网络工具 / 密钥与存储 / 代码执行 / 提示词数据流 / 供应链),全部关键发现经人工复核代码证据;未安装依赖、未执行代码。
  • 重要声明:dsh 是"模型可执行任意代码"的 agent 框架,模型跑代码本身是特性。本报告只关注护栏能否被绕过以及实现缺陷。

一、总体结论

代码整体防御意识强:沙箱 fail-closed(不可用时拒绝执行而非降级)、审批门控、原子写、路径规范化、浏览器信任围栏(Host + sec-fetch-site + Origin)、密钥文件 0600/0700、出厂 bundle 显式关闭 SSRF 原语、会话日志 seq 校验。没有发现可远程(跨网络/跨浏览器)利用的 critical 漏洞——默认部署只监听 127.0.0.1 且 DNS rebinding 被 Host 围栏挡住。

但存在 1 个 critical 级设计缺陷,它把上面大部分护栏架空了:

本地 Web 网关完全无认证,而沙箱不隔离网络 → 沙箱里的模型自己就是"可信本地进程",可以自答审批、冒充人类、偷走存储的 API key。

以及若干 high 级问题:默认挂载的 run_code 以裸 worker 线程全 Node 权限执行模型代码(绕过沙箱);可选挂载的"模型自写插件"在进程内以完整权限运行且无审批;会话策略日志可被伪造;沙箱运行器存在 PATH 劫持面;文件写栅栏存在 TOCTOU。

严重级别统计(复核后):Critical 1,High 8,Medium 12,Low 12,Info 7。另有 2 条子代理上报的 DNS-rebinding 发现经复核为不成立(见 §九)。


二、Critical:审批自答绕过(PDF-01,默认部署可达)

位置:packages/host/apiproxy/src/fetch/handler.ts:283-317(/api/respond 仅媒体类型围栏)、packages/host/apiproxy/src/api-proxy.ts:3696-3710(respond 用 rpcId+approvalId+sessionId 结算)、packages/host/apiproxy/src/api-proxy.ts:1443-1467(审批帧经 mux SSE 广播)、packages/sandbox/sandbox-local/src/profiles.ts:16-23(bwrap 无 --unshare-net)。

链条(已逐环节验证):

  1. 所有沙箱 profile 只约束文件效果,网络完全不受限(bwrap 仅 --ro-bind / / --dev /dev --proc /proc --die-with-parent,无 --unshare-net;Seatbelt/Landlock 仅文件规则)。文档明确"Network and process visibility are outside this vocabulary"。
  2. Web 网关的信任围栏(packages/client/connection/src/api-request-trust.ts)只验证 Host 是回环/trustedHosts + 浏览器标记——任何本地进程发的 Host: 127.0.0.1:3080 无 Origin 的请求直接通过。围栏注释自述"this fence is not an auth layer"。
  3. 模型触发一次需要审批的操作(如 bash 沙箱升级 sandbox_permissions)→ approveEscalation → approval.request → 待决审批(rpcId/approvalId/sessionId)经 /api/events.mux SSE/WebSocket 广播。
  4. 模型在沙箱内 curl/Node fetch(Node 22 内置 WebSocket)连回 127.0.0.1:3080,读到自己会话的待决审批 id,然后 POST /api/respond 提交 outcome: allowed-once → 审批被结算为通过。

影响:人类审批(这是 dsh 对危险操作的核心安全控制)被模型自己静默绕过——模型可自批 danger-full-access 后任意读写主机。任何其他本地进程同样可以。默认部署即可达,无需任何非默认配置。

建议:网关加每实例随机 bearer token(注入 UI 会话,模型工具不可见);审批响应要求不可伪造的人类手势证明(如仅在浏览器 UI 会话内一次性 token);沙箱加 --unshare-net 或至少阻止工具命令访问 127.0.0.1:3080。


三、High 发现(按主题)

主题 A:无认证本地网关的连锁后果(默认可达,本地进程/模型即可利用)

ID 问题 位置 要点
PDF-02 session.prompt 可向任意会话注入 source: {kind:'user'} 消息(steer/followup),会话记录与导出可无认证读取 api-proxy.ts:2461-2516、fetch/handler.ts:91、:260-271 模型或本地进程可冒充人类向任何会话投递指令、读取全部对话与导出日志,审计无法区分
PDF-03 llm.discoverModels 接受调用者 baseURL,省略 apiKey 时回退存储的密钥,随请求发往调用者指定 URL api-proxy.ts:3404-3426、packages/llm/llm-pi-ai/src/discovery.ts:232-252 一次未认证调用即可把真实 API key 外传(PRIVILEGED_METHODS 仅限回环——但模型本身就在回环内)
PDF-04 同上入口即 SSRF:${baseURL}/models 由宿主进程 GET,可探测/读取内网与云元数据 discovery.ts:86-88,232-252 响应体回显给调用者
SEC-01 网关整体无认证(GET 通道同样只有围栏),本地进程可读全部会话日志、credentials.set 覆盖存储的 API key fetch/handler.ts:254-286 本地恶意软件/其他用户进程即得会话全文与凭据篡改能力
SEC-06 /api/session.export 无所有权/工作区授权,沿完整 subagent 谱系打包导出 session-export.ts:219-266 一个调用导出跨工作区全部会话与附件
CE-02 Typert Remote 面 POST /api/<ns>/<method> 未认证;动态 Cordis 授权 token 是顺序计数器(run-1/pkg-1/approval-1),inventory() 泄露 agentId → 可猜解 resolveRequestRun 自批插件激活、invoke 驱动宿主处理器 packages/api/gateway/src/index.ts:104-110、cordis-host-runner/src/index.ts:412-416,740-766 LAN(trustedHosts/0.0.0.0)部署下远程可打;回环下本地进程可打

复核说明:PRIVILEGED_METHODS(connection/src/index.ts:89-119)把 settings./credentials./llm.discoverModels 钉在回环——对浏览器/跨站无效,但对"模型自己"无效,因为模型就是回环进程。

主题 B:沙箱与代码执行边界

ID 问题 位置 要点
CE-03 run_code 在默认 bundle 挂载(web-app:48-49 挂 dsh-code-runtime-worker-thread),模型程序在裸 worker 线程执行:完整 Node 全局(fs/child_process/fetch/process.getBuiltinModule),空 env + heap 上限外无任何 OS 沙箱、无审批——而 bash 工具反而是被沙箱约束的 packages/code-runtime/code-runtime-worker-thread/src/index.ts:378-393、core/tools/src/code-mode.ts 模型代码 = 任意主机代码执行,绕过 workspace-write 约束;模块自述"containment, not a security boundary"
CE-04 workflow 引擎:模型写的脚本经 vm 空上下文执行,但注入的宿主闭包(agent/parallel/phase/log)可 fn.constructor('return process') 逃逸到宿主 realm → 完整 Node 能力 packages/workflow/workflow-worker-thread/src/runtime.ts:100-113 与 CE-03 同级别影响,无审批
CE-01 (条件触发) 模型自写 Cordis Host 插件:tool-cordis(cordis_define/cordis_run)仅存在于 examples,一旦启用:host-only 包直接 activate 无审批(index.ts:270-275);node:vm 自述"not containment"(sandbox.ts:8-9,注入 btoa 等宿主闭包可 btoa.constructor('return process')());ctx façade 转发任意 inject 声明服务(guard.ts:738-744),含 credentials.resolve(读密钥)、无约束 fs、settings、web cordis-host-runner/src/index.ts、sandbox.ts、guard.ts 提示词注入/子代理失守即可进程内任意代码 + 取密钥,无任何审批
CE-07 动态插件 façade 的"声明式白名单"实为无白名单——inject:['credentials'] 即得密钥读写 guard.ts:709-711,738-744 CE-01 的服务转发面
SANDBOX-01 (条件触发)bwrap/sandbox-exec 按裸名经 PATH 解析(sandbox-local/src/index.ts:338-340,68-74),PATH 又特意保留在子进程 env 中(subprocess/src/index.ts:60-66)。若 PATH 含模型可写目录(工作区=PATH 组件、共享目录、PATH 含 .),模型植入假 bwrap → 探测通过 → 沙箱静默失效 sandbox-local/src/index.ts Landlock/Windows 运行器按绝对路径固定不受影响

主题 C:策略与日志完整性

ID 问题 位置 要点
SANDBOX-02 / PDF-06 sandbox/mode、approval/policy、permission/preset 全部从会话 JSONL 折叠(最后一个事件生效),而日志加载只校验 seq 连续性(chunk-rows.ts:339-345 非 chunk 行原样透传)、无 MAC/完整性保护。官方示例(headless/jsonrpc-agent)把日志放 root: './.sessions'(模型可写的工作区内)→ 模型重写日志注入 danger-full-access + never,重启后免审批全权限 session-mode.ts:52-57、user-approval/src/index.ts:112-118、session-persistence-jsonl/src/format.ts:362-375 默认 web bundle 用 dshHomePath('sessions')(工作区外)不受影响;审计轨迹可一并伪造
SEC-05 凭据 provider 把项目 .env 作为受信回退层,而项目目录正是模型可读(通常可写)的工作区——恶意仓库可替换模型实际使用的 API key credentials-local/src/index.ts:260-263 凭据替换/提取
SEC-02 settings 秘密脱敏走查器对 union/transform 节点原样返回(代码自带 TODO),role(‘secret’) 值可经 settings.describe 上 wire settings/src/redact.ts:50-91 应 fail-closed

主题 D:文件系统

ID 问题 位置 要点
FS-01 / SANDBOX-06 写沙箱 TOCTOU:checkedTarget 规范化+包含性检查后把同一路径字符串交给原子写,检查与 rename() 之间窗口可被祖先符号链接替换;工具自身提供并发原语(background bash 无超时)可赢竞态 → 写任意用户可写路径(如 ~/.ssh/authorized_keys) fs-sandbox/src/index.ts:126-148、fs-local/src/fsio.ts:533-595 需模型主动构造竞态;文档称"accepted residual"但与 checkedTarget 注释声称的"no TOCTOU"矛盾
FS-02 读路径在所有沙箱模式下不受限;glob/grep 以 --no-ignore --hidden 全盘扫描(glob path=/ 即枚举全盘含 dotfile) fs-sandbox/src/index.ts:7-8、tool-fs-search/src/glob.ts:90-107 与 bash 沙箱读全盘设计一致,但使任何提示词注入立即变全盘机密外传
FS-03 str_replace_editor 无界整文件读入内存、无超时(对照 read 工具 ≥10MiB 走流式+50KiB 上限)→ OOM/挂起 tool-str-replace-editor/src/index.ts:234,291,342 DoS
FS-04 读后写保护是组合约定非强制不变量(waterfall 默认回退=无条件覆盖) tool-fs/src/write.ts:111、docs/subsystems/filesystem.md:185 缺插件/注册序变即静默失效
SANDBOX-03 进程组级终止可被 setsid() 绕过:孤儿进程逃过 job_kill 与宿主退出 subprocess-local/src/spawn.ts:260-267 需 /proc 树或 subreaper
SANDBOX-04 子进程 env 凭据清洗按名称模式(/KEY|PASSWORD|SECRET|TOKEN/i),DATABASE_URL/*_DSN/MONGO_URI 等连接串全透传 subprocess/src/index.ts:44,60-66 模型 echo $DATABASE_URL 即可外传内嵌凭据

主题 E:Web 细节

ID 问题 位置 要点
WEB-01 /plugins/events(HMR SSE)是唯一未过围栏的路由,connections 无上限 → 本地进程连接洪泛 DoS + 插件图泄露 packages/client/hmr/src/index.ts:165-179 其他所有 /api 路由都过围栏(connection/src/index.ts:161-168)
WEB-03 settings.mutate 路径操作原型污染(__proto__ 键触发继承 setter;代码自带 TODO) settings/src/index.ts:215-228,297-305 仅回环可达
WEB-04 WS 下行/FrameQueue 无背压与缓冲上限(SSE 路径有 drain 背压,WS 没有)→ 慢消费者内存 DoS websocket-downlink.ts:118-137、api-proxy.ts:414-445 本地
WEB-05 /api 请求体全量缓冲 160MiB、无请求超时 http-bridge.ts:8-12,54-66 本地并发大上传可耗内存
WEB-02 browse 模式 host.listDirectory/createDirectory 未入 PRIVILEGED_METHODS(LAN 部署下全盘目录枚举/mkdir);代码注释承认"能建会话者本就能以本进程身份跑命令" connection/src/index.ts:89-119、directory-picker-browse/src/index.ts:217-323 围栏不一致而非边界突破

主题 F:网络工具(多数为已知限制/条件触发)

ID 问题 位置 要点
NET-01 web_fetch 是无目的地校验的 SSRF 原语(仅限 scheme/长度/userinfo;可打 127.0.0.1、RFC1918、169.254.169.254 云元数据、混淆 IP)。复核更正:包级默认 fetch: true,但出厂 bundle 显式 fetch: false 且不挂载 fetch provider(bundle/base/cordis.patch.yml 注释明确"该 provider 推迟了 SSRF 防护")——默认关闭、README 有硬警告;启用即高危 web-fetch-http/src/policy.ts:24-41 属文档化已知限制,但"启用即裸奔"值得知晓
NET-02 重定向同源校验是主机名字符串比较,DNS rebinding 可绕过(仅在有 NET-01 前提时相关) provider.ts:76-96 与 NET-01 同属 fetch 原语
NET-03 MCP 桥把远端服务器工具描述/schema 原样注入模型工具集,无信任标记/能力隔离,且服务器可运行时 tools/list_changed 换工具——经典 MCP 提示词注入面 mcp-client/src/tools.ts:146-152、connection.ts:257-270 非默认挂载
NET-05 搜索 provider 把 API key 发给任意 URL.canParse 通过的 baseURL(含 http:// 明文、env 可覆盖) web-search-*/src/provider.ts 配置受信前提下低危
NET-07 LSP/MCP stdio 服务器进程无沙箱以完整用户权限在主机启动,仅 PATH 存在性校验 lsp-stdio/src/connection.ts:92-104 配置受信;PATH 影子可致模型触发任意二进制执行

主题 G:供应链(无 critical/high)

ID 问题 位置 要点
SC-01 PyPI 发布用可变分支 tag action(pypa/gh-action-pypi-publish@release/v1)且作业带 id-token: write .github/workflows/python-release.yml:206,241 应钉 SHA
SC-03 本地发布脚本 publish-npm-baseline.ts 默认把整个 workspace 发布到外部域名 registry.npm.harnessment.com(仓库内无该域名属主记录),仅校验 http(s) scripts/publish-npm-baseline.ts:23,1037 建议删除或硬限制 allow-list
SC-04 node-pty 补丁让 DSH_NODE_PTY_SPAWN_HELPER 环境变量决定 PTY helper 二进制路径,无完整性校验(path.resolve 后直接执行) patches/node-pty@1.1.0.patch:14-21 能影响 env 者即可让 harness 以用户权限执行任意二进制
SC-05 发布 pack 脚本对用户提供的输出目录 rm -rf 无目标确认 scripts/release/pack.ts:51、native/landlock-run/scripts/pack-release.mjs:47 破坏性脚枪
SC-02/06/07/08/09 全部 workflow 用可变 tag 而非 SHA;vendored Cordis 深度分叉且发布物无审计线索;Wine 门禁拉 latest Node;GitLab tag 正则即发布;pkg 构建期网络拉取基础二进制 — 卫生类

四、子代理上报但经复核不成立的发现

  1. PDF-05 / SEC-01 的 DNS-rebinding 绕过("恶意网页可经 DNS rebinding 接管整个 harness"):不成立。isTrustedApiRequest(api-request-trust.ts:96-123)的 Host 围栏正是 DNS-rebinding 防御——rebinding 页面的 Host 头是攻击者域名,被直接 403。跨站 POST 也被"只收 application/json + 从不回 OPTIONS"挡掉。网页端攻击路径不存在;真正的问题是"任何本地进程(含沙箱模型)",而非网页。
  2. NET-01 的"默认启用":包级默认 true,但出厂 bundle 显式禁用(fetch: false 且无 provider)——已按实际修正为"条件触发"。

五、修复优先级建议

P0(默认部署即存在,立即处理)

  1. 本地网关认证:每实例随机 bearer token(注入 index.html/UI 会话,对模型工具隐藏)+ 审批/问题应答要求浏览器会话内的一次性凭据(PDF-01/02/03、SEC-01)。
  2. 沙箱隔离网络或封锁工具进程访问 127.0.0.1:3080(--unshare-net 或 bwrap 内 deny loopback)(PDF-01 的另一半)。

P1(默认或常见部署可触达)
3. run_code 套用与 bash 相同的沙箱/审批策略,或默认降级为受限能力(CE-03)。
4. workflow 引擎移除宿主闭包注入或独立沙箱化(CE-04)。
5. llm.discoverModels 的 baseURL 加 https+allowlist、禁止回环/私网;存储密钥绝不随调用者 URL 外发(PDF-03/04)。
6. 会话日志加完整性保护(HMAC/密钥在日志目录外),策略折叠前校验事件模式(SANDBOX-02/PDF-06)。

P2(条件触发/加固)
7. 沙箱运行器绝对路径解析(SANDBOX-01);fs 写栅栏改 fd 基元关闭 TOCTOU(FS-01)。
8. 动态插件:host-only 包同等审批、ctx façade 改 deny-by-default 服务白名单、vm 换真隔离(CE-01/07/08)。
9. env 清洗按值形态补连接串(SANDBOX-04);__proto__ 键拒绝(WEB-03);HMR 路由加围栏(WEB-01)。
10. 供应链:action 钉 SHA、publish 脚本 allow-list、node-pty env 路径约束(SC-01/03/04)。


六、审计局限

  • 纯静态分析,未运行代码/未验证 PoC 的运行时可行性(如 TOCTOU 竞态概率、审批 id 获取的具体难度)。
  • 未做依赖 CVE 扫描(node_modules 未安装,pnpm audit 不可行)——建议在 CI 中启用 pnpm audit/osv-scanner。
  • 覆盖深度按攻击面加权,packages/host/apiproxy(3744 行)、extensions/cordis-host-runner(3360 行)等大文件由子代理+抽查复核,未逐行人工审计。
  • 示例配置(examples/*)中的用法未逐一核对与文档的一致性。
正文完
 0