沙箱里那条 find 删掉了 3187 个文件:工具调用沙箱的三层防线怎么被逐个绕过

举报
yd_238528621 发表于 2026/11/04 12:11:36 2026/11/04
【摘要】 给内部运维 Agent 挂 shell 工具三周后,上线第 19 天出事:一条 find 把 -mtime +7 写成 -7,删掉 /data/tmp 下 3187 个文件,其中 412 个是当天还在写的。复盘发现三层防线逐个失效,白名单只认命令名不认参数、容器挂了宿主可写目录、超时只杀了父进程。改完四道闸后六周里 dry-run 阶段拦下 11 次参数级高危调用。

上个季度我给内部运维 Agent 挂了一个 shell 执行工具,前后花三周做"沙箱",自以为想得挺全:命令白名单、容器隔离、执行超时,一样不少。上线第 19 天,它收到一句"帮我清一下 /data/tmp 里的临时文件",然后删掉 3187 个文件,其中 412 个是当晚还在写的业务临时文件。

事后复盘,三道防线全部失效,而且每一道失效都很朴素,既没有零日漏洞,也不是模型越狱。下面是当时的现场和三处具体的漏,以及我后来重做的四道闸。

一、现场:312 毫秒,3187 个 inode

先看日志。从工具调用发出到 find 返回,312 毫秒。容器里被 unlink 的 inode 一共 3187 个,其中 412 个文件的 mtime 落在当天 18:00 之后。

模型生成的命令是这个样子:

  • find /data/tmp -name "*.log" -mtime -7 -delete
  • 它想表达的语义是"7 天以前的日志"
  • 实际语义是"7 天以内的日志"
  • 一个 + 写成了 -,删除方向完全反过来

这条命令没有触犯任何一条我们写在策略文件里的规则。它用的是白名单里的命令,没有 rm,没有 dd,没有管道拼 shell。从校验器视角看,这是一条完全合规的调用。

二、第一层失效:白名单只认命令名

我们第一版的策略是一张命令白名单:find、grep、du、df 放行,rm、dd、mkfs、shutdown 直接拒绝。

看起来抓住了要害,实际只覆盖了最笨的一类错误。find 在白名单里,参数由模型自己生成,参数错了照样执行。这等于一间只没收了电锯、却放行所有锤子的屋子。

顺着这个思路往下想会更不安,类似的"白名单内的杀伤力"不止 find 一个:

  • awk 可以在 BEGIN 里调 system(),直接拼出任意 shell 语句
  • sed 带 e 修饰符能把匹配内容当命令执行
  • xargs 接受任何后续命令,白名单形同虚设
  • sh -c 更直接,一条白名单命令就能变成任意脚本

结论很直接:一个只检查命令名的沙箱,等于没有沙箱。真正决定破坏半径的是参数和目标路径,不是命令本身。

顺带说一句实现上的教训。我们最早想用正则匹配命令字符串,试了一下午就放弃了。find /data/tmp -name '*.log' -exec rm {} \; 这种写法,引号里套命令、反斜杠转义分号,正则根本切不开 token 边界,改成整段字符串模糊匹配又会把正常命令误杀。最后落在 shlex 词法解析上,先把命令拆成 token 序列,再按 token 位置匹配参数模式,才勉强可维护。

三、第二层失效:容器隔离,但挂载把墙拆了

第二版我们上了容器,逻辑上应该"跑在盒子里"。启动参数里有这么一行:-v /data/tmp:/data/tmp,为的是让容器能读业务日志。

只读标志忘了加。

于是沙箱里的 find 操作的其实是宿主机的 /data/tmp。隔离层存在,通往外面的门开着。这一层最容易被忽略,因为 docker run 成功、容器内 ls 正常、进程树看起来都在容器里,所有表征都是"隔离生效"。

真正要命的是我们的排查顺序。出事当晚我先查了容器配置,看到 mount 是通的、路径一致,反而更放心了,因为"路径对上了"。看着对,恰恰是错。

四、第三层失效:超时杀的是父进程

第三版加执行超时,写法是 timeout 30 sh -c "$cmd"。

日志显示它确实在 30 秒时发了 SIGTERM,但收到信号的是 sh。find 作为子进程活在另一个进程组里,收到 SIGTERM 之后继续扫剩下的目录,直到自己跑完。我们的"30 秒上限"实际上限了个寂寞。

后来查进程树才看清整条链路,timeout 只盯住它直接启动的那个进程,sh -c 再 fork 出去的孙进程完全不在它的射程里。修法不复杂:setsid 起新会话,超时用 kill -- -PGID 把整个进程组打掉。

五、重做的四道闸

事故后把校验器重写了一遍,现在是四道:

  • 参数级校验。白名单从"命令"升级成"命令 + 参数模式":find 必须同时给出显式 -mtime 与路径前缀,命中 -delete 时必须带 -print 先打印,awk 的 system(、sed 的 e 修饰符、xargs 一律判死。这一道就拦掉了当天那条命令。
  • 强制两阶段。所有写操作先 dry-run,输出待影响对象清单,命中数量超过 200 条时必须人工确认才进第二段。这个阈值是拍的,参考点是当天误删 3187 个、日常正常清理通常几十个。
  • 挂载收紧。宿主目录只做只读 bind mount,可写范围只留容器内 scratch,写完再由外层按规则回传。
  • 进程组级超时 + 全量留档。命令、退出码、stdout/stderr、影响对象清单全部入库,出事当天不用再靠猜。

改完到现在六周,同类参数级高危调用在 dry-run 阶段被拦下 11 次,其中 3 次是 -mtime 正负号写反,2 次是路径变量拼成了 /。一次都没到真正执行。

小结

工具调用沙箱的难点从来不在"要不要隔离",而在隔离之后的边界是不是真的闭合。

  • 白名单要落到参数,光看命令名等于没看
  • 容器要检查挂载,只读标志漏一个就等于裸奔
  • 超时要打进程组,父进程死了不代表活干完了
  • 破坏性操作必须能先看一眼影响范围,再看不清就不许动手

一句提醒,跟我当初的想法正相反:做沙箱最该花的力气,不在拦住模型不该做的事,而在确保模型做了不该做的事时,损失被限制在一个能算清的范围内。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。