文:AI行业技术交流
2026-08-13 · 针对 deepseek-harness 0.1.0-rc.5(pre-release,无外部消费者)· 四个 PoC 全部在真实代码路径上验证通过 摘要:本文记录对 DeepSeek Harness(基于 Cordis 的插件化 agent harness)的一次安全设计评审。我们从四个 PoC 出发,展示三条独立漏洞(!!js 配置求值、read-only 沙箱全盘可读、动态插件 vm 沙箱 exec 逃逸)以及一条链式利用(exec 逃逸 → 裸 subprocess → 无约束宿主进程执行),并说明这些漏洞如何在真实攻击场景中被外部利用,以及我们如何系统地发现它们。所有发现均附 file:line 证据与可复现脚本。0. 背景:被评审的系统与威胁模型
DeepSeek Harness 是一个"万物皆插件"的 agent harness:
- 插件树:模型适配器、工具注册表、session 日志、agent loop 本身都是 Cordis 插件,可通过配置组合与替换;
- 事件溯源:session 日志是 append-only 的,模型可见的一切都必须能从日志重建("model-visible means logged");
- capability seam:每个可替换能力由 Service Definition / Provider / Consumer 三角色组成;
- 沙箱分层:bash 执行经由
ctx.sandbox用 landlock/seatbelt/bwrap 包 argv;文件系统由进程内 fence 强制写边界;权限模式有read-onlyworkspace-writedanger-full-access三档; - 动态插件:模型可以通过
cordis_definecordis_run工具写代码,挂载为自己的插件,在一个node:vm"沙箱"里运行,通过守卫后的 facade 访问服务。
威胁模型:模型是半可信主体。它可能被 prompt injection(恶意网页、恶意仓库内容、被 fetch 的文档)诱导,也可能在 read-only 模式下被部署去处理不可信仓库——"只读"是 marketed 的安全承诺。攻击者的终极目标:在宿主机上执行任意代码,或外泄凭据。
我们的评审方法见第 6 节;先看漏洞本身。
1. PoC V1:!!js 配置求值 = 加载期同步代码执行机制
cordis.yml / cordis.patch.yml 支持 !!js 表达式。Loader 在挂载每个配置行时,通过 internal/config waterfall 求值它:
vendor/loader/src/index.ts:92-101 ctx.on('internal/config', ...) → interpolate(this.ctx, config)vendor/loader/src/config/utils.ts:5-9 evaluate = new Function('ctx','expr','with (ctx) { return eval(expr) }')
表达式在 with(ctx) 作用域链内做直接 eval;process 在 ctx 上查不到,沿作用域链落到 Node 全局。仓库的 engines 范围是 node ^22.19 || >=24,而 Node 22.3+ 提供process.getBuiltinModule()——同步取任意内建模块,不需要 require/import。于是一个 !!js 就是一段同步的、加载期的、任意代码执行:
- id: anythingname: any-pluginconfig:probe: !!js process.getBuiltinModule('node:fs').writeFileSync('/tmp/pwned', 'x')
值得注意的是,PROFILE_PATCH_TEMPLATE 的注释还主动告诉用户"!!js expressions allowed"(app-boot/src/profile.ts:127-131),而 app-boot 用 HMR 监听 patch 文件(app-boot/src/index.ts:213)——写入即执行,每次 boot 重放。
PoC 验证
走真实 Loader 挂载路径(loader.create 一个带 __jsExpr 序列化形式的行):
外部利用场景与防线现状[V1] marker written at config-load time: true[V1] marker content: ”executed-at-config-load”[V1] writableRoots(workspace-write): [”/tmp/dsh-poc-v1-workspace”,”/private/tmp”,”/private/var/folders/...”]
默认部署下,模型的可写面是 workspace-write = workspace root + 系统临时目录(roots.ts:52-55),而 patch 文件在 ~/.dsh/profiles//cordis.patch.yml——默认写边界恰好挡住。但防线只有这一层,且是一个无校验的不变量:
- 没有任何 boot-time 检查保证"已加载的配置目录 ∉ 可写 root"。
workspaceRoot配到 home、把 profile 放进工作区、self-modification 部署(该仓库的demo:cordis就是这个场景)、danger-full-access模式,任何一条静默闭合链条; - 即便写不进 patch 文件:第 4 节的链式利用会让写边界整体失效。
利用场景:配置投毒(恶意仓库诱导模型改配置)、自修改部署下的持久化后门。
2. PoC V2:"read-only" 沙箱读穿全盘机制
read-only 模式对写执行严格 fence,对读完全不设防:
- 进程内 fence(fs-sandbox)只包
writeTexteditText(fs-sandbox/src/index.ts:91,112),readText原样继承、无任何检查; - 进程级 bwrap 配置是
--ro-bind / /(sandbox-local/src/profiles.ts:17)——整个宿主文件系统只读挂载进沙箱。
read-only 模式的可写 root 是空集(roots.ts:52-55),但它同时意味着:模型可以读 ~/.ssh/*、项目 .env、credentials 文件——而 credentials 系统自己用 0600 模式保护这些文件(credentials-local/src/index.ts:103-122),说明威胁模型里它们就是秘密。
PoC 验证
真实 SandboxPolicyService + SandboxedFileSystem,read-only 模式,读 workspace 外的"凭据"文件:
插件正门:inject 无 allowlist[V2] read of a secret OUTSIDE the workspace under read-only mode:”apiKey: dsh-poc-secret-value\n”[V2] contrast — write under the same mode denied with code: FS_SANDBOX_DENIED[V2] writableRoots(read-only): [][V2] process level: bwrap profile is --ro-bind / / (profiles.ts:17)
动态插件声明注入的服务时,唯一检查是"服务是否存在"(cordis-host-runner/src/lifecycle.ts:56),没有允许清单。服务包装 guardedService 只拒绝"返回值是 Context"(guard.ts:669-697),不挡任何读写。于是模型写的插件可以从前门声明:
inject: ['fs', 'web'] // read-only 下 fs.readText 无 fence,web 可外发读 secret + 外发,一步到位。叠加另一个事实:默认组合里没有任何 host 侧插件对工具调用发射 ask——只有 CC/Codex hook 桥把 hook 返回的 'ask' 映射进来,dsh-user-approval 提供的是服务而非默认接线,tool-bash 里还留着 TODO(permissions)(tool-bash/src/index.ts:6-7)。也就是说,"只读安全模式"下模型读凭据是零审批的。
利用场景:在 read-only 模式部署 harness 处理不可信仓库/代码评审——这正是该模式最可能的使用场景——仓库内容诱导模型读 .env、~/.ssh、云凭据并外发。
3. PoC V4:动态插件 vm 沙箱的 exec 逃逸背景:团队已经封堵了一整类逃逸
cordis-host-runner 的守卫并不粗糙。sandbox-context.spec.ts 的测试标题就是"escape surface is closed",覆盖了:ctx.root/ctx.parent/ctx.scope 等框架成员拒绝访问、ctx.extend/ctx.isolate 拒绝、服务返回 Context 的拒绝(denyContext)、tools.get 只返回 schema 视图不返回可调用的 ToolDefinition……这些都是认真做过的对抗性设计。
漏洞:execute(args, exec) 这条路径漏了
但守卫只覆盖了 apply(ctx) 的 facade,没覆盖execute(args, exec)的 exec:
guard.ts:582-584 execute(args, exec) { return cloneJson(await rawExecute(args, exec), ...) }exec(ToolRunContext)原样透传给 vm realm 里的模型代码,而 exec.agent.ctx 是真实的运行时 Context(runtime-types.ts:64-75 声明 readonly ctx: Context)。facade 防住了语法面,没防住对象图。
PoC 验证
走完整真实路径:runner.define → run 挂载 → 真实 registry 管线执行 → vm realm 代码经 exec.agent.ctx.get('pocSecretStore') 取到宿主服务:
4. PoC V4b(链式):exec 逃逸 → 裸 subprocess → 无约束 RCE为什么这个链成立:约束在错误的层[V4] dynamic package mounted and running: true[V4] isError: false[V4] sandbox-realm tool code retrieved from the host Context: ”HOST_SECRET_LEAKED”
沙箱约束(landlock/seatbelt/bwrap 的 argv 包装)只存在于shell 层——bash-sandbox 在调 ctx.subprocess.spawn 之前包 argv(bash-sandbox/src/index.ts:177-179)。而 subprocess seam 自身对约束一无所知:subprocess-local 里没有任何 sandbox 引用。这是 seam 结构的经典问题:enforcement 落在某个 consumer 上,而不是落在执行点上——其他调用者(或拿到 ctx 的逃逸代码)直接绕过。
PoC 验证
vm 沙箱里的模型代码,经逃逸 ctx 拿到裸 subprocess 服务,spawn 宿主进程:
[V4b] dynamic package mounted and running: true[V4b] tool result (isError=false): ”spawned-and-exited”[V4b] unconfined host process wrote marker: true[V4b] marker content: ”unconfined-spawn\n”
无文件写入、无审批、无沙箱边界跨越——这一步之后,写 patch 文件(!!js 持久化)、改任何文件、跑任何命令,全部开放。第 1 节的写边界防线被整体绕过。
一条被"偶然"堵住的路:loader.create 与 realm 边界
我们还尝试了更隐蔽的链:逃逸 ctx 拿 loader 服务 → loader.create 一个带 __jsExpr 的行。结果是:行被真实挂载了(报错是重复注册 timer 服务),但 !!js 没执行。原因值得记录:interpolate 的 isJsExpr 检查 value instanceof Object(config/utils.ts:26),而 vm-realm 构造的 config 对象不是宿主Object的实例——realm 边界偶然挡住了这条链。这是脆弱防御:只要 isJsExpr 改成 typeof value === 'object',或 config 先经过宿主 clone,路径立即打开。它也不影响结论——subprocess 链根本不需要 !!js。
端到端攻击链
默认组合下动态插件是否真的零审批?恶意网页 / 仓库内容 / fetch 的文档│ prompt injection▼模型调用 cordis_define + cordis_run(默认组合零审批)│▼vm 沙箱运行模型代码 → execute(args, exec) 拿到宿主 exec(guard.ts:582-584)│▼exec.agent.ctx.get('subprocess').spawn(...) —— 无 landlock/seatbelt/bwrap│▼宿主机任意进程执行 → 持久化(patch 文件 + !!js)→ 长期驻留
在我们的 PoC 中,runner.define + runner.run 在没有任何 answerer的树上直接成功——测试基建(helpers.ts)的 gateway 只记录事件,不回答也放行。web 组合下存在"双勾"授权流,但其授权粒度是包级的(一次批准放行该包的未来版本,cordis-host-runner/src/index.ts:240,278),与"每次请求单独授权"的 seam 语义不一致。
5. 外部利用场景汇总
#攻击者入口链条影响默认部署可达性Aprompt injection(恶意网页/仓库)→ 模型cordis_define → V4 exec 逃逸 → V4b 裸 subprocess宿主 RCE,零文件写入是(无需任何配置失误)Bread-only 模式处理不可信仓库bashcat/ fs read 工具 / 插件inject:['fs','web']凭据外泄(.ssh/.env/credentials)是(read-only 部署即中)C配置投毒模型写 patch 文件 →!!js→ HMR 执行 + 每次 boot 重放持久化 RCE条件性(workspaceRoot 覆盖配置目录 / self-modification 部署)D诱导模型skill 内容(声明式,本身不执行代码)诱导模型走 A/B 路径同 A/B是(放大器,非独立面)
关键点:场景 A 不依赖任何部署配置失误。V1 单独评估时"默认部署下链是断的"的结论,在 V4b 面前不成立——写边界在插件路径上完全不参与。
6. 如何发现这些漏洞:方法论复盘
这次评审的完整流程,按顺序:
6.1 读架构文档,列"信任假设清单"
先精读架构文档(architecture.md、glossary、capability-seams、cookbook),目标不是理解怎么用,而是找出系统在哪些地方假设了模型配合。列出的清单直接变成了候选漏洞:
- "配置里允许
!!js"——假设了配置文件的写入者可信; - "read-only 是安全模式"——假设了读不可怕;
- "动态插件运行在受限环境"——假设了 vm 边界真的限制;
- "sandbox 约束 shell 执行"——假设了所有执行都走 shell。
每条假设对应一个待验证的攻击面。
6.2 分域并行审计:五个方向,各带问题清单
把代码库分成五个域,并行审计(每个域独立产出 file:line 证据):
- core loop + session 日志:状态机、输入丢失、崩溃恢复、事件溯源成本;
- capability seam:Provider 替换承诺是否成立、policy 落在哪一层、单实现 seam 的真实契约;
- 工具管线与安全:approval 承载什么、restriction 是过滤器还是 enforcement、沙箱逃逸面;
- 编排:subagent 组合、并行冲突、预算语义;
- typert/RPC/wire:类型图、错误码、帧放大、无界缓冲。
关键技巧:enforcement 落点图。对每条安全属性,标出它在哪里被强制(provider?consumer?约定?),凡是落在 consumer 或文档语气里的,标记为高风险候选——这直接命中了 V2(观察策略只在工具层)和 V4b(沙箱约束只在 bash 层)。
6.3 用仓库自己的规则当检测器
这个仓库有极其明确的工程规则(CLAUDE.md):"Enforce a decision in the operation that makes it"、"Misconfiguration fails loud"、"Apply bounds to the complete result"。把这些规则反过来当检测器:找出违反它们的地方,就是漏洞候选。V2(读不 fence = 策略没在执行点强制)和 V4b(约束在 consumer 不在 provider)都是这条规则的反面教材。用被审代码自己的标准攻击它,结论最有说服力。
6.4 先验证原语,再分层 PoC
不直接写大 PoC,先逐个验证最小的"原语假设",每个假设都是后续链条的一环:
with(ctx)+eval引擎里process.getBuiltinModule可用?(node -e 一行验证)ToolExecutionInput.agent存在、registry 接受 agent?(读类型 + 测试基建)loader.create会触发 interpolate?(直接跑,marker 落盘)writableRoots的语义与 patch 文件位置的关系?(读 roots.ts + profile.ts)
然后分层 PoC,每层都比上一层更接近真实攻击:
- 机制级:真实 Loader 挂载路径上让
!!js写 marker(V1); - 管线级:完整 runner → registry → 工具执行路径上让 vm 代码取到宿主服务(V4);
- 链式:把两条链接起来,证明端到端可达(V4b)。
全部用真实代码路径,零 mock。测试基建直接复用仓库自己的 helpers 模式(fs-sandbox.spec.ts 的 boot 写法、cordis-host-runner 的 setup),这保证了"如果 PoC 通过,漏洞就在产品里"。
6.5 失败路径也是证据
V4b 第一次尝试(loader.create 带 __jsExpr)失败了——但失败原因本身就是发现:vm-realm 对象过不了宿主 instanceof Object。如实记录失败路径,得到两个收益:(1) 发现一条"偶然防御",评估它的脆弱性;(2) 转向 subprocess 路线,找到更直接的链。安全评审里被堵住的路和走通的路一样有价值。
6.6 可复用检查清单(适用于任何 agent harness)
- 配置求值面:配置文件里有没有代码求值钩子(!!js / template / plugin 引用)?谁能写这些文件?写入面与加载面之间有没有不变量校验?
- 读/写不对称:安全模式对写做 fence 时,读是否被同等对待?secret 目录是否在挂载/读取面内?
- facade 的对象图:任何"受限环境"的守卫,检查它防的是语法还是对象图——被守卫的上下文中是否有对象引用(exec、agent、服务返回值)能走回真实运行时?
- enforcement 落点:每条安全策略在 provider/执行点强制,还是在 consumer/约定层?枚举直接调用者,看谁能绕过。
- 注入面的下游能力:工具结果裸进上下文时,模型(被注入后)能触达的最大权限是什么?按"注入 → 模型 → 能力 → 影响"画攻击链。
- 授权语义:审批请求承载了什么(参数?哈希?还是只有理由)?授权粒度是 per-request 还是 per-package?
优先级修复封堵的路径P0给动态工具execute传剥离agent.ctx的 facade exec(局部改动,guard.ts)V4 本体 + V4b 整条链 + 插件侧 V1/V2P0SubprocessRuntime自身承担 confine 责任:约束在 seam 的执行点落地,而不是依赖"只有 bash-sandbox 调它"的约定V4b 的结构性根因P1动态插件 inject allowlist(白名单收敛),define/run 接入审批管线V2 插件正门、场景 A 的零审批入口P1read-only 排除 secret 目录;把默认 approval 真正接进tools/pre-execute(现在只有 hook 桥发射 ask)场景 B 的凭据外泄P2boot 时校验"已加载配置目录 ∉ writableRoots",相交即 fail loud场景 C 的纵深防御
8. 附录:PoC 清单与复现
文件验证目标关键输出/tmp/dsh-pocs/poc-v1-js-config-exec.ts!!js加载期同步代码执行(真实 Loader)marker written at config-load time: true/tmp/dsh-pocs/poc-v2-readonly-reads.tsread-only 读穿全盘(真实 fs 插件)读 secret 成功 / 写被 FS_SANDBOX_DENIED/tmp/dsh-pocs/poc-v4-exec-escape.tsvm 沙箱 exec 逃逸(完整 runner→registry 管线)"HOST_SECRET_LEAKED"/tmp/dsh-pocs/poc-v4b-exec-escape-to-rce.ts逃逸 → 裸 subprocess → 无约束宿主进程unconfined host process wrote marker: true
复现步骤:
cd /path/to/deepseek-harness# 仓库源码,pnpm install 完成pnpm exec tsx /tmp/dsh-pocs/poc-v1-js-config-exec.tspnpm exec tsx /tmp/dsh-pocs/poc-v2-readonly-reads.tspnpm exec tsx /tmp/dsh-pocs/poc-v4-exec-escape.tspnpm exec tsx /tmp/dsh-pocs/poc-v4b-exec-escape-to-rce.ts
要求:Node ≥ 22.3(仓库 engines ^22.19 || >=24),脚本经仓库 tsconfig paths 解析源码,每个脚本自清理 fixture。修复落地后,这些脚本可直接改写成对应包的回归测试(V4 放入 cordis-host-runner/tests/ 尤其合适,正好补上 sandbox-context.spec.ts 未覆盖的 exec 面)。
9. 结语
三个漏洞的共同根因可以压缩成一句话:安全属性的 enforcement 落在了错误的层——配置求值信任了写入者、只读模式信任了"读不可怕"、vm 沙箱防了语法没防对象图、进程约束落在了一个 consumer 而不是执行点。而链式利用告诉我们:在 agent harness 里评估单个漏洞的"可达性"时必须画到端到端——V1 单看被写边界挡住,V4b 让这道防线根本不在路径上。
对这类系统的防御者,最大的一条建议:把"模型被注入后能走的最远路径"当作系统测试用例来写,而不是当作威胁模型文档来写。
代码仓库:https://github.com/zzszmyf/dsh-security-pocs
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.