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)→ approveEscalationapproval.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-02session.prompt 可向任意会话注入 source: {kind:'user'} 消息(steer/followup),会话记录与导出可无认证读取api-proxy.ts:2461-2516fetch/handler.ts:91:260-271模型或本地进程可冒充人类向任何会话投递指令、读取全部对话与导出日志,审计无法区分
PDF-03llm.discoverModels 接受调用者 baseURL,省略 apiKey 时回退存储的密钥,随请求发往调用者指定 URLapi-proxy.ts:3404-3426packages/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 keyfetch/handler.ts:254-286本地恶意软件/其他用户进程即得会话全文与凭据篡改能力
SEC-06/api/session.export 无所有权/工作区授权,沿完整 subagent 谱系打包导出session-export.ts:219-266一个调用导出跨工作区全部会话与附件
CE-02Typert Remote 面 POST /api/<ns>/<method> 未认证;动态 Cordis 授权 token 是顺序计数器(run-1/pkg-1/approval-1),inventory() 泄露 agentId → 可猜解 resolveRequestRun 自批插件激活、invoke 驱动宿主处理器packages/api/gateway/src/index.ts:104-110cordis-host-runner/src/index.ts:412-416,740-766LAN(trustedHosts/0.0.0.0)部署下远程可打;回环下本地进程可打

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

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

ID问题位置要点
CE-03run_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-393core/tools/src/code-mode.ts模型代码 = 任意主机代码执行,绕过 workspace-write 约束;模块自述"containment, not a security boundary"
CE-04workflow 引擎:模型写的脚本经 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、webcordis-host-runner/src/index.tssandbox.tsguard.ts提示词注入/子代理失守即可进程内任意代码 + 取密钥,无任何审批
CE-07动态插件 façade 的"声明式白名单"实为无白名单——inject:['credentials'] 即得密钥读写guard.ts:709-711,738-744CE-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.tsLandlock/Windows 运行器按绝对路径固定不受影响

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

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

主题 D:文件系统

ID问题位置要点
FS-01 / SANDBOX-06写沙箱 TOCTOU:checkedTarget 规范化+包含性检查后把同一路径字符串交给原子写,检查与 rename() 之间窗口可被祖先符号链接替换;工具自身提供并发原语(background bash 无超时)可赢竞态 → 写任意用户可写路径(如 ~/.ssh/authorized_keysfs-sandbox/src/index.ts:126-148fs-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-8tool-fs-search/src/glob.ts:90-107与 bash 沙箱读全盘设计一致,但使任何提示词注入立即变全盘机密外传
FS-03str_replace_editor 无界整文件读入内存、无超时(对照 read 工具 ≥10MiB 走流式+50KiB 上限)→ OOM/挂起tool-str-replace-editor/src/index.ts:234,291,342DoS
FS-04读后写保护是组合约定非强制不变量(waterfall 默认回退=无条件覆盖)tool-fs/src/write.ts:111docs/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-03settings.mutate 路径操作原型污染(__proto__ 键触发继承 setter;代码自带 TODO)settings/src/index.ts:215-228,297-305仅回环可达
WEB-04WS 下行/FrameQueue 无背压与缓冲上限(SSE 路径有 drain 背压,WS 没有)→ 慢消费者内存 DoSwebsocket-downlink.ts:118-137api-proxy.ts:414-445本地
WEB-05/api 请求体全量缓冲 160MiB、无请求超时http-bridge.ts:8-12,54-66本地并发大上传可耗内存
WEB-02browse 模式 host.listDirectory/createDirectory 未入 PRIVILEGED_METHODS(LAN 部署下全盘目录枚举/mkdir);代码注释承认"能建会话者本就能以本进程身份跑命令"connection/src/index.ts:89-119directory-picker-browse/src/index.ts:217-323围栏不一致而非边界突破

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

ID问题位置要点
NET-01web_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-03MCP 桥把远端服务器工具描述/schema 原样注入模型工具集,无信任标记/能力隔离,且服务器可运行时 tools/list_changed 换工具——经典 MCP 提示词注入面mcp-client/src/tools.ts:146-152connection.ts:257-270非默认挂载
NET-05搜索 provider 把 API key 发给任意 URL.canParse 通过的 baseURL(含 http:// 明文、env 可覆盖)web-search-*/src/provider.ts配置受信前提下低危
NET-07LSP/MCP stdio 服务器进程无沙箱以完整用户权限在主机启动,仅 PATH 存在性校验lsp-stdio/src/connection.ts:92-104配置受信;PATH 影子可致模型触发任意二进制执行

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

ID问题位置要点
SC-01PyPI 发布用可变分支 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-04node-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:51native/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"):不成立isTrustedApiRequestapi-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(默认或常见部署可触达)

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

P2(条件触发/加固)

  1. 沙箱运行器绝对路径解析(SANDBOX-01);fs 写栅栏改 fd 基元关闭 TOCTOU(FS-01)。
  2. 动态插件:host-only 包同等审批、ctx façade 改 deny-by-default 服务白名单、vm 换真隔离(CE-01/07/08)。
  3. env 清洗按值形态补连接串(SANDBOX-04);__proto__ 键拒绝(WEB-03);HMR 路由加围栏(WEB-01)。
  4. 供应链: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/*)中的用法未逐一核对与文档的一致性。