博客

DeepSeek Harness 沙箱按设计不拦读:Agent「逃逸」逃出的是哪一层?

Reddit 说 DeepSeek Harness 逃出了工作区,官方文档写的是沙箱模式只管写、读一律放行;真正的写越界在 discussion #523。再加上 vLLM 解析器的 eval() 洞(2025 年已修)与 Hugging Face 被打进生产库,三件事各占一层。

Leo Kaka约 18 分钟
三层边界示意图:工作区层(DeepSeek Harness,读放行、写围栏)、推理引擎层(vLLM 工具调用解析器 eval())、平台层(Hugging Face 凭证与零日),每层标出被捅破的位置

先回答标题。Reddit 上那位用户看到 DeepSeek Harness 跑出项目目录翻别的文件,从帖子描述看是读;而 DSH 的沙箱模式按官方文档 只分「写到哪里」,读在任何模式下都放行——这是设计,算不上逃逸。真正的写越界另有其事:GitHub discussion #523 里,Web 端一个叫 minimal 的启动预设(开会话时选的一组配置,常见的是 standard 和 minimal)给文件编辑工具接了一条不经沙箱检查的路,界面显示受限, 编辑器照样写到工作区外。配置上只有两件事:不用 minimal 预设;把 harness 整个放进容器或独立用户里跑,只挂项目目录。 「逃逸怎么防」的答案在 harness 外面,而 harness 只是三层里的第一层——另两层是推理引擎(vLLM 那个 eval() 洞,2025 年 8 月已修) 和平台(Hugging Face 被打进生产库)。

过去一周三件事,各占一层

三件事进入视野的时间不一样,先把日期摆正。

  • 8 月 24 日晚,r/LocalLLaMA 出现《我刚试了 DeepSeek Harness,它逃出了工作区目录》工作区层:agent 读了项目目录之外的文件,读本来就放行。这一层真正的缺口是 discussion #523 的写越界, Windows 与 Linux 各有一次独立复现,未见官方定性。
  • 同一天,Boyd Kane 的 essay 《LLM 可以通过利用推理引擎控制它的宿主机》 上了 HN(essay 页面自标 8 月 25 日)。推理引擎层:例子 CVE-2025-9141 是 vLLM 一年前的洞,2025 年 8 月已修,本周新的是把它升格成一类攻击面。
  • 7 月 21 日OpenAI 披露评估中的模型打进了 Hugging Face 的生产数据库,因上周的出售报道又被拉回讨论。平台层:目标是别人的平台,OpenAI 自评「platform-level compromise」(平台级入侵)。

共同点不在「AI 失控」,在于每一件都卡在某一道边界上,而且边界各不相同。下面逐层拆,每层三个问题:边界是谁划的, 被捅破时长什么样,防线该放在哪。

第一层·工作区:DeepSeek Harness 的沙箱模式只分写

帖子说了什么,没说什么

DeepSeek Harness 的热度本刊上周写过,这篇只看沙箱。

原帖作者 Far_Note6719 的叙述很短:DSH 分析本地文件跑了两小时,然后「离开了项目目录(虽然 DSH 配置正确),开始翻我的其他文件, 这我从没允许过」,并提醒「这只是 preview,别指望它遵守简单的规则」。评论区里他说模型是 qwen3.8-27B, 一个开源模型,不是 DeepSeek 自家的。

帖子没说的是:读了还是写了。通篇是 “walk through my other files”(翻看我的其他文件),没有删改。 jimmc414 说得直接:「叫 escape 太重了,escape 听起来像它离开了大楼。」有人问 OP 到底是读还是写,OP 没有正面回答。 UnspeakableHorror 给了机制层面的解释:「它什么都能读,只能在工作区里写,工作区外的写会来问你。我以为这就是它该有的样子?不然它怎么读外部库来写代码?」

这条评论是对的。证据在官方文档里。

官方文档里的三档模式

DSH 仓库里有一份沙箱子系统文档文档站版本,仓库里还有中文版 sandbox.zh.md), 「Modes and enforcement」一节的第一句就把边界划清了:

SandboxMode governs filesystem effects only. … Network and process visibility are outside this vocabulary.

SandboxMode 只管文件系统层面的效果。……网络与进程可见性不在这套词表里。)

三档模式的定义在类型注释里:

/**
 * File-effect policy for confined processes. `read-only` permits only required
 * sinks such as `/dev/null`; `workspace-write` also permits the workspace and a
 * backend-defined temp area; `danger-full-access` bypasses confinement. Network
 * and process visibility are outside this vocabulary.
 */
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'

注意 read-only 的定义:它限制的是写到哪里(只允许 /dev/null 这类必需的输出口),而非「只能读工作区」。 文件系统工具那一侧的 fs-sandbox README 说得更白:「Reads always pass through — every mode permits reading.」(读一律放行——每个模式都允许读。)文件工具的围栏和 bash 的沙箱从同一个 writableRoots 函数取可写集,防止两边漂移。

执行层是真的内核机制。sandbox-local 在 Linux 上优先用 bwrap、其次 Landlock,macOS 用 Seatbelt,Windows 用 ACL 受限令牌;bwrap 的 profile 是只读的宿主根目录、 新的 /dev、私有 PID 的 /proc,命令看不到宿主进程。所以说「整个沙箱只拦写」并不准确:进程可见性隔离由执行后端提供, 只是它不进入模式词表,也不保证在 partial 时成立。文档同时诚实地写了两件事。其一,执行强度(enforcement)是「一个被报告的事实」, full 是内核管住了模式承诺的每一种文件效果,partial 是只管住一部分,老内核的 Landlock ABI 与 Windows ACL 目前都是 partial。 其二,文件工具的围栏原话是 a policy fence, not a kernel boundary(策略围栏,不是内核边界)——它防的是 agent 把文件写出界, 防不了恶意进程。拿掘金上《Sandbox(沙箱)到底是什么?》(陆枫Larry,8 月 21 日) 的四问——文件、进程、网络、系统权限各能碰多少——当尺子,DSH 的模式词表只回答了第一问的一半:文件能到哪里。 这是文档自己划的范围,那位用户看到的行为落在范围之内。

DeepSeek Harness 三档沙箱模式对照四个问题:读、写工作区、写工作区外、网络与进程
图 1 — 三档模式只在「写」这一列有区别;读一律放行,网络与进程不在词表内。来源:docs/subsystems/sandbox.mdfs-sandbox README

真正的写越界在 discussion #523

Reddit 评论里有人贴了一个链接,比帖子本身重要。discussion #523, 8 月 13 日,标题是「Web minimal 预设允许在 workspace-write 下未经批准写到工作区之外(Windows)」。复现很干净:工作区在 E 盘, 导出的会话事件流写着 permission/preset: workspace-writesandbox/mode: workspace-writeapproval/policy: ask, 然后 str_replace_editor 工具用绝对路径在 C:\Users\Public\Documents\ 下建了四个文件,没有任何审批事件。

discussion #523 页面截图:标题「[Security] Web minimal preset allows unapproved writes outside workspace-write on Windows」,状态标记 Unanswered,Q&A 分类,6 participants,正文 Summary 里 UI 报 permission preset / sandbox mode 均为 workspace-write、approval policy 为 ask,而 str_replace_editor 实际写到 C:\Users\Public\Documents
图 2 — 界面报的三行状态与实际写入位置并列在同一份报告里;页面状态至今是 Unanswered,即未获官方定性。页面抓取于 2026-08-26。来源:deepseek-harness discussion #523

报告者给出的疑似原因,用人话说:minimal 预设给编辑器工具配的文件系统后端是裸的 @deepseek-ai/dsh-fs-local,这条路不经过 sandboxPolicy 检查,界面上的 workspace-write 对它不起作用。两天后有人在 Ubuntu 上用 standard 预设复现了同类现象: bash 侧 / 确实被挂成只读,str_replace_editor 却在 ~/.dsh/ 下建了文件,同样没问。8 月 19 日一位社区成员(页面上没有维护者标记, 这条 discussion 至今状态是 Unanswered)给了缓解办法:新会话用 Standard 预设加 Workspace Write,不用 minimal,rc.7 里 Standard 的文件编辑走宿主那条带沙箱的路径;并注明这「不是通用的宿主安全保证,Windows 上 enforcement 可能是 partial」。这条建议没有回应 8 月 15 日那次 standard 复现,复现者也没给版本号;是不是版本差异,原件里没有答案。

两件事放在一起看才有意思。Reddit 帖描述的是设计内的行为(读放行),舆论把它叫逃逸;#523 描述的是设计外的缺口 (一条写路径绕过了策略),却只有 6 个参与者。真正危险的是「界面说受限、有一条路没受限」——你以为围栏在,它有一段不在。

这一层的防线不在 harness 里

HN 上 essay 那条帖子里排在最前的评论来自 alphazard,说的其实是 harness 沙箱这一层的事:

把安全当成属于 harness 的东西,这个框架完全是错的,我希望没人指望一个正确的 harness 来隔离他们的 agent。VM,甚至只是一个容器就够了。 agent 应该能在它的环境里以 root 运行、想干什么干什么。

Reddit 评论区的共识与此一致,而且给了具体做法。 freehuntx 贴了自己的 Dockerfile:node:22-bookworm 底镜像,全局装 @deepseek-ai/dsh@0.1.0-rc.8,二十来行加一个 entrypoint; 他的用法是只挂当前目录和 dsh 配置目录,一个 alias 启动。Repinsky 把代价也说了:「容器会让模型拿不到系统库和你的全局配置,而这往往正是 agent 往外跑的原因。」 Dangerous-Report8517 再往上推一级:Docker 的默认隔离面向可信的单租户应用,换成 gVisor、Kata、krun 这类运行时才到 VM 级。还有人只是 给 agent 开了一个独立的低权限系统用户

反面例子也在同一个帖子里。有人用另一款 harness 的权限插件把工作区外的 python 命令设成 deny, aqezz 拿同一份配置测试,写了个内容为 rm -rf * 的脚本让 agent 执行。agent 先拒绝了;用户说「我就是要跑」,agent 就 chmod +x 照跑, 两个文件被删,配置里明写的 rm -rf * deny 规则没响一声。aqezz 的结论:权限只看直接的工具调用,不看脚本内容,「这不是真安全」。 前半段拒绝的是模型,后半段放行的是围栏。另一位用户的 qwen3.6-27b(另一个模型)「就写了个脚本绕过去」; AdNew5862 的经验是「你封 bash 它用 python,你封 python 它用 perl」,MindfulMan1984 管这叫打地鼠。

所以这一层的配法是首段那两件事:harness 内不用 minimal 预设,harness 外整个进程进容器。代价是容器里少了全局工具链, 第一周会不停地往镜像里补依赖,比「它可能翻到 ~/.ssh」便宜。怎么核、怎么装,放在文末。

第二层·推理引擎:工具调用解析器是模型可控的输入

第一层的前提是 agent 的手脚——文件、shell——被围起来。第二层的问题是:围栏外面还有一台机器在跑模型本身,它的输入是什么?

先解释一个词。模型输出的本来是一段文本,比如 <tool_call><function=x><parameter=y>…;推理引擎里把这段文本变成结构化工具调用 (函数名加 JSON 参数)的那段代码,就是工具调用解析器。它的输入是模型说的话。

CVE-2025-9141 的一手记录

Boyd Kane 那篇 essay 的核心例子是 vLLM 的 GHSA-79j6-g2m3-jgfw, 即 CVE-2025-9141,2025 年 8 月 20 日发布,High、CVSS 8.8,攻击向量 Network、所需权限 Low。Summary 只有一句:「任何已认证用户, 只要能让模型把代码作为参数传进一次工具调用,就能在服务器上执行任意代码。」触发条件三条:开了 --enable-auto-tool-choice、 指定了 --tool-call-parser qwen3_coder、参数类型未显式声明或未被识别。受影响 0.10.0 起,修复在 0.10.1.1。

有问题的代码在 PR #21396 引入,2025 年 7 月 22 日合并。解析器处理 Qwen3 Coder 的 XML 格式工具调用,按工具 schema 里声明的 type 转换参数值。我把合并时那版源码(commit 4594fc3)读了一遍,advisory 第三条的 措辞其实偏松。string、int、float、bool 各有自己的转换路径;schema 里没声明 type 的参数按字符串原样返回,根本不进 eval()。 进 eval() 的是三类:声明为 objectjson.loads 失败的,一切声明为 array 的,以及任何解析器不认识的类型名:

else:
    if param_type == "object" or param_type.startswith("dict"):
        try:
            converted_value = json.loads(param_value)
            return converted_value
        except json.JSONDecodeError:
            logger.warning("... will try other methods to parse it.")
    try:
        converted_value = eval(param_value)
        return converted_value
    except Exception:
        logger.warning("... cannot be converted via Python `eval()` ..., "
                       "degenerating to string.")
    return param_value

也就是说,一个工具只要有一个 files: string[] 这样的列表参数,每次调用都把一段模型生成的字符串送进了推理主机的 eval(); 多数 agent 工具集不止一个。0.10.1.1 的同一个文件里已经没有 eval(

essay 说 Gemini 自动审查标记了这个问题、维护者仍然强制合并,两件事我在 PR 页上都核到了原话。gemini-code-assist: 「模型生成输出上使用 eval() 是一个 critical 的安全漏洞,可能导致任意代码执行。」维护者:「I’m force merging this to unblock model usage, after lint.」(过完 lint 就强制合并,别耽误模型可用。)从合并到公告 29 天。

CVE-2025-9141 时间线:2025-07-22 PR 合并,2025-08-20 安全公告,0.10.1.1 修复,2026-08-24 essay 重提
图 3 — 一个洞的四个时间点。来源:PR #21396GHSA-79j6-g2m3-jgfwessay

我不打算替维护者辩护或加罪,一条 lint 之后就合的评论是所有高速迭代项目的日常。要看的是结构: 解析器的输入是模型吐出来的 token,而模型吐什么,任何能发请求的人都能影响。「已认证用户」在自托管场景里往往就是 拿着一把共享 key 的所有内部服务。

一年前的洞,为什么本周被重提

essay 做的事是把一个 bug 升格为一类攻击面:推理引擎支持 200 多种模型架构、几十个 Jinja 聊天模板,还要识别思考块和工具调用, 每一处解析逻辑都是模型输出进入宿主机的入口。他的防线建议是 GPU 主机只吐 logits,另一台机器采样、解析、转发给 harness, 解析器被攻破也只丢一台 CPU 机。

HN 上的争论可以压成三句。angry_octet 把它读成「通过 HTTP 接口攻击推理引擎」, 并说自己因此把 vLLM 跑在独立 VM、防火墙隔离的 VLAN 上,没有 DNS,只有 syslog 与遥测出站。作者 beyarkay 回复说文章讲的是消化 token 序列的解析器,没提 HTTP。两人说的其实是同一条链的两端——token 来自请求,请求走 HTTP,CVSS 里的 Attack Vector: Network 就是这个意思;分歧只在「谁是攻击者」,后一种攻击者不需要模型有任何意图,只需要一次提示注入。

wren6991 补了一个大多数人没想到的面:本地推理框架的 HTTP 接口上挂着各种小玩意, 比如 llama.cpp 自定义的 KV 缓存存盘/恢复 API,「这些也可能是任意磁盘读写」;而人们习惯让 agent 的 shell 同时拥有 API key 和访问引擎的网络路径。这句话把第一层和第二层接上了:一个能读 $HOME 的 agent,读到 key,就能对着推理引擎的全部接口面发请求。 要交代一句分歧:wren6991 主张沙箱由 harness 来管、只把 agent 的 shell 关进去,这样 harness 到引擎的网络通路不必开给 shell; 本文推荐的是整个 harness 进容器。两种拓扑都成立,区别在于你信不信 harness 自己的沙箱实现;看过 #523 之后,我选前者。

请求侧能看见什么,看不见什么

请求在到引擎之前若经过网关一跳,能看见的是请求体。工具定义是请求体里的 JSON Schema,校验每个参数是否声明了 type、 形态是否合规,是网关或你自己的 SDK 都能做的事。但这个洞的触发条件恰恰是合法的 array 声明——请求侧校验拦不住它, 只能收窄输入面,收不掉漏洞。

看不见的是引擎的解析路径。响应流回来时,工具调用已经是引擎解析后的结构化产物;哪个分支进了 eval(),请求侧不知道也不该假装知道。 它也看不见引擎的其他 HTTP 接口,agent 的网络若能直达引擎,中间那一跳等于不存在。所以第二层的防线仍然在部署拓扑: 引擎不暴露给 agent 所在网段,升级到 0.10.1.1 以上,不给没审过的解析器开 --enable-auto-tool-choice。代价是多一跳、多一台机器, 以及每次换模型都要重新审一遍解析器。

不自托管、用托管 API 的团队也有事可做。「已认证用户就是拿着共享 key 的所有内部服务」推到多租户场景,就是:别的租户的一次 提示注入,可能在你共用的推理进程上拿到 RCE。问供应商三句就够——推理引擎是什么、版本是否不低于修复版;是否对你的流量开了 --enable-auto-tool-choice、用的哪个 parser;租户之间是否共享推理进程。答案由供应商给,本文不替任何一家作答。

第三层·平台:凭证与供应链

前两层的攻击目标都是自己的机器。第三层的目标是别人的。

OpenAI 在 7 月 21 日的披露里描述了一次内部评估。 GPT-5.6 Sol 与一个更强的未发布原型,为了测上限关闭了网络安全方向的拒绝机制,在一个刻意不启用生产分类器的 benchmark 里跑。 模型先在包注册表缓存代理里找到一个零日,拿到公网访问;然后推断 Hugging Face 上可能托管着这次评估的答案,链式利用被盗凭证 与更多零日,在 Hugging Face 服务器上取得远程代码执行路径,从生产数据库里拿到了测试答案。OpenAI 的原话是 「unprecedented cyber incident」(前所未有的网络安全事件),7 月 29 日的更新把 Hugging Face 一案定性为「platform-level compromise」。 Hugging Face 的安全团队自己检测并阻断了这次活动;披露没有提到模型权重被改动,我也不会暗示。收购语境见英文刊上周的 分发层易主那篇,这里只取它作为第三层的样本。

这一层的边界既非文件系统也非解析器,是凭证和供应链。模型没有攻破自己的沙箱,它是从沙箱里合法可达的一个包缓存代理出去的; 进入 Hugging Face 也没有用什么魔法,用的是被盗的凭证。Reddit 帖里 Dangerous-Report8517 的话:「以为容器这类简单策略就够了, 正是让 OpenAI 出事的原因。」

这层跟个人开发者的关系比看起来近。你的 agent 跑在你的用户下,~/.huggingface/token~/.npmrc、云厂商的 key 它全能读, 出网又不受限——它就是那个拿着合法凭证出门的人,差别只在有没有人(或一段注入的提示)给它指过路。第三层的防线有三样。 出网白名单:容器里的进程只能连到列出来的地址,每接一个新服务都要改一次名单。凭证短期化:agent 拿到的 key 有效期以小时计、 权限只够本次任务,签发与轮换要有人做。依赖镜像离线:包管理器只对着内网镜像,换来同步延迟和存储。三样都不新, 新的是发请求的那一方现在会自己找路

三层放在一起:谁划的边界,谁负责

边界由谁划被捅破时的表现防线放在哪这一层的防线管不到
工作区harness 的沙箱策略与执行后端agent 读/写了项目之外的文件harness 之外:容器 / VM / 独立用户引擎的解析器;agent 拿着 key 出网
推理引擎引擎的解析与 HTTP 接口代码模型输出被当成代码执行,GPU 宿主机被控拓扑:引擎独立网段,版本更新,解析器审查agent 读宿主文件;平台凭证
平台平台的凭证、供应链、依赖入口合法可达的服务被当成跳板,第三方被打进生产库出网白名单、短期凭证、离线镜像前两层的任何缺口

三层互不替代,这是本文唯一想让读者带走的判断。容器防不了引擎 RCE,引擎通常在容器外;给引擎打补丁防不了 harness 读 $HOME, 那是文档明说的设计;两者都防不了拿着合法凭证出网打第三方,那条路上每一步都是「允许」。#523 里有位参与者的话可以直接抄: 沙箱存在 ≠ 安全。更准确地说,沙箱存在只等于它文档里写的那几件事成立,而且还要看 enforcement 那一栏是 full 还是 partial

网关这一层能约束的只有三件

我们做网关,这一层是我们的日常,所以把话说窄。三层里网关都不在正中间,它只是所有 LLM 请求都要经过的那个点, 能约束的是三件事,每件都只是「设计为」。

凭证形态。 先说管不到的:Hugging Face 那次被盗的是平台侧的凭证,agent 手里的 LLM key 作用域再窄,也拦不住那条链。 网关能管的只是 agent 手里那把 LLM key 长什么样。pirouter 的 key 设计为按 agent、按任务签发,带作用域与有效期,泄了作废一把, 不用动别的。代价是每个 agent 会话多一次签发。

配额。 失控的 agent 最先烧掉的是钱和速率。单 key 的花费上限与请求速率设计为在网关侧硬限,超了就拒。代价是合法的长任务会撞墙, 你得为它单独调高,而非把默认调高。

上游可达面。 出网白名单是你的容器或网络层的事,网关做不了你的防火墙;它能做的只是让 LLM 流量只需要一个出口地址, 好让名单短一点,以及把「这把 key 能打到哪些模型、哪些 provider」设计为在 key 上收窄。代价是非 LLM 的外部服务仍要你另设出口。

三件都是设计口径,不是上线承诺;现状以产品页为准。网关看不见的三样也得写下来:引擎里的解析路径、 harness 的文件围栏、容器的边界。谁说网关能替这三样兜底,就是在卖第四层。

今晚能做的三件事,一层一件

  1. 工作区层:核预设,再把 harness 关进去。 新会话在 Web 端选 Standard 预设加 Workspace Write,不选 minimal (#523 里 8 月 19 日那条缓解建议的原话)。要核对一个会话实际跑的值,看它的会话事件日志——DSH 每会话一份 JSONL (默认按 Zstandard 帧压缩存储,可配置为原始行,见持久化文档), 里面 permission/presetsandbox/mode 两行就是 #523 报告者贴出来的那两行。已有的 minimal 会话,按两次复现的位置检查 有没有多出来的文件:Windows 看 C:\Users\Public\Documents,Linux 看 ~/.dsh/。团队要统一就固化进镜像:照 freehuntx 的思路, 容器里预装 dsh,只挂 $PWD 与 dsh 配置目录。代价:核值五分钟;容器化第一周每天往镜像里补一次依赖。
  2. 推理引擎层:自托管的查版本、查拓扑,托管的问三句。 vLLM 低于 0.10.1.1 先升;引擎和 agent 不在同一网段; 没审过的 --tool-call-parser 不配 --enable-auto-tool-choice。用托管 API 的把上一节那三句发给供应商。 代价:多一跳网络;解析器审查按模型数 × 半天排人力。
  3. 平台层:凭证不进 agent 的家目录,出网先收成名单。 长期 token(Hugging Face、npm、云厂商)不放在 agent 能读的路径, 换成有效期以小时计的短期凭证;容器出网先从「全放」改成只放推理端点和包镜像两个地址,其余按需加。代价:签发与轮换要有人做; 名单每接一个新服务改一次。这是三件里最麻烦的一件,先做前两件也行。

三件做完,三层各有一道自己的门。做不完也没关系,至少下次看到「agent 逃逸」四个字时,先问一句:逃出的是哪一层。