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)。
链条(已逐环节验证):
- 所有沙箱 profile 只约束文件效果,网络完全不受限(bwrap 仅
--ro-bind / / --dev /dev --proc /proc --die-with-parent,无--unshare-net;Seatbelt/Landlock 仅文件规则)。文档明确"Network and process visibility are outside this vocabulary"。 - 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"。 - 模型触发一次需要审批的操作(如 bash 沙箱升级
sandbox_permissions)→approveEscalation→approval.request→ 待决审批(rpcId/approvalId/sessionId)经/api/events.muxSSE/WebSocket 广播。 - 模型在沙箱内
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 构建期网络拉取基础二进制 | — | 卫生类 |
四、子代理上报但经复核不成立的发现
- 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"挡掉。网页端攻击路径不存在;真正的问题是"任何本地进程(含沙箱模型)",而非网页。 - NET-01 的"默认启用":包级默认 true,但出厂 bundle 显式禁用(
fetch: false且无 provider)——已按实际修正为"条件触发"。
五、修复优先级建议
P0(默认部署即存在,立即处理)
- 本地网关认证:每实例随机 bearer token(注入 index.html/UI 会话,对模型工具隐藏)+ 审批/问题应答要求浏览器会话内的一次性凭据(PDF-01/02/03、SEC-01)。
- 沙箱隔离网络或封锁工具进程访问 127.0.0.1:3080(
--unshare-net或 bwrap 内 deny loopback)(PDF-01 的另一半)。
P1(默认或常见部署可触达)
run_code套用与 bash 相同的沙箱/审批策略,或默认降级为受限能力(CE-03)。- workflow 引擎移除宿主闭包注入或独立沙箱化(CE-04)。
llm.discoverModels的 baseURL 加 https+allowlist、禁止回环/私网;存储密钥绝不随调用者 URL 外发(PDF-03/04)。- 会话日志加完整性保护(HMAC/密钥在日志目录外),策略折叠前校验事件模式(SANDBOX-02/PDF-06)。
P2(条件触发/加固)
- 沙箱运行器绝对路径解析(SANDBOX-01);fs 写栅栏改 fd 基元关闭 TOCTOU(FS-01)。
- 动态插件:host-only 包同等审批、ctx façade 改 deny-by-default 服务白名单、vm 换真隔离(CE-01/07/08)。
- env 清洗按值形态补连接串(SANDBOX-04);
__proto__键拒绝(WEB-03);HMR 路由加围栏(WEB-01)。 - 供应链: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/*)中的用法未逐一核对与文档的一致性。