AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


DifySandbox 的构建背景和实现机制
发布日期:2024-07-12 10:10:59 浏览次数: 2922 来源:Dify


Hello,这里是 Dify 的后端工程师 Yeuoly,也是 DifySandbox 的作者。对于 Dify 的社区用户来说,应该对一个叫做 Sandbox 的 docker 服务不陌生,我们也收到过很多关于 Sandbox 的反馈,但是绝大多数用户对于 Sandbox 本身非常陌生,并不清楚 Sandbox 的内部细节,而这篇文章将会让你逐渐了解 Sandbox 内部究竟发生了什么。

DifySandbox:基于执行代码和系统安全需求

在 Dify 中,Workflow 是一项重要的能力,它允许用户以拖拉拽的方式来编排一个逻辑流,从而实现相对复杂的业务逻辑,在编排逻辑的过程中,复杂的数据处理是必不可少的,具体来说,我们可能有下面这些场景:

  • 有时候我们需要处理 LLM 生成的 JSON 文本,从中提取结构化的数据,有时候又需要处理 HTTP 请求返回的 XML 文本、JSON 文本。

  • 在另外一些场景下,甚至需要合并两个知识检索节点的输出内容,亦或者是合并知识检索节点 GoogleSearch 的结果。

  • 甚至一些情况,具备编码基础的用户想要使用一些例如 Jinja2 和 Liquid 这样的模板语法,从而可以实现更灵活的 prompt 编排。

上述的这些场景是多而杂的,但是究其根本仍然是数据处理,它们需要一个统一的解决方案,那么自然而然的我们就会想到写代码来实现,因为相比于数据处理节点这类高度定制化的实现,代码显然会更通用,如果给用户提供一个代码编辑框,在编辑框内,用户可以通过编写自己的代码来实现数据处理逻辑,那么这些问题就都迎刃而解。

既然需要在 Dify 中执行用户编写的代码,自然而然就需要考虑安全的问题,面对恶意用户的时候,代码执行就不再是一个正常功能,而变成了一个漏洞。

在理想情况下,用户编写的代码大多都是非恶意的代码,如果直接由 Dify 来执行代码,那么其实就是让 Dify 在服务器上创建一个新的 Python、Nodejs 进程,并将用户的代码提交到这个进程里去执行,就像下面这样,Process 就是执行代码的进程,User 需要首先访问 Dify,再由 Dify 来访问 Process。

而在恶意用户的手上,上面这个过程就会出现问题,Process 是直接运行在服务器上的,这会造成 Process 可以访问文件系统和数据库,那么此时就变成了下面这样了,用户编写的代码可以任意读取服务器上的文件,甚至在极端情况下可以读取或者删除整个 Dify 的数据库。

正是因为这样的问题,Dify 自研了 DifySandbox,它是一套代码沙箱方案,可以有效拦截恶意代码的操作,并放行正常业务逻辑,下面我们将详细介绍 Sandbox。

DifySandbox 的设计思路和实现机制

在设计之初,我们从几个角度考虑了 Sandbox 所需要满足的安全性能:

  1. 我们考虑了全球开发者不同的编码倾向,总得来看,在 LLM 的生态里,Python 和 Nodejs 两者是绝对的 Top2,同时,我们也不希望强制用户编写 Python 或者 Nodejs,因此,我们希望将两者都提供给大家,所以 Sandbox 的技术方案不能局限于某一个语言,应该有一个在系统层级上的解决方案。
  2. Sandbox 存在被绕过的可能性吗?这肯定是无法绝对避免的,世界上没有绝对安全的系统,因此我们不能将安全性完全依赖于 Sandbox 本身,应该做到即使 Sandbox 存在漏洞,黑客也无法访问核心资源。
  3. 网络是一个老大难的问题,它的影响是方方面面的:

    1. 生产中很多的沙盒绕过都是通过网络,如 VMWare 虚拟机,它挺多的漏洞都是通过网络来实现的。

    2. 即使在代码层面、系统层面实现了防护,仍然阻止不了恶意代码通过网络来嗅探内网,从而非法访问内网资源,因此 Sandbox 还需要考虑如何隔离网络。

同时,我们也调研了市面上现有的一些沙盒方案,它们和其优缺点大概如下:
  1. WebAssembly:这是一个目前大多数沙盒的方案,通过将 Python、Nodejs 的解释器编译为一个 WebAssembly 的运行时,从而可以在 Nodejs 中或者浏览器中运行 Python 代码,在确保规范使用 WebAssembly 的情况下,这个方案在系统层面是非常安全的,但是它带来的问题就是自由度不够高,比如说如果需要安装第三方依赖,那么这样做起来就会有非常多像架构不兼容这样的深坑,并且要同时为 Python 和 Nodejs 做不同的处理,并不是一个特别灵活的方案。
  2. Docker:有一些厂家使用了运行一次代码就新建一个 Docker 容器的做法,这样做的自由度肯定是非常高的,但是代价就是运行速度极慢,运行一次代码需要 1s+ 的时间,最终在 workflow 上体现出来可能就是 10 个代码节点就消耗了  10s+ 的时间,这样的代价实在是太大,同时,这个方案还需要管理容器,需要将docker daemon的sock挂载到 Dify 的 api 容器中,这带来了极大的安全隐患,或者使用docker in docker的解决方案,但是这就更慢了,不是一个理想的解决方案。
  3. 特定语言的沙盒包:Nodejs 下有 vm2,Python 下有 Pypy,但是他们都限制在了某一个语言上,并且都有各自的限制,并不是一个通用方案,并且他们在处理依赖的时候也需要按照其规范处理,以及像 Pypy 的 Python 版本也有严格的限制,依赖也不是那么得方便,当然这里还存在一些其他的库,如 pyodide,不过最大的问题还是 Nodejs 和 Python 的方案不通用,维护起来有一定难度。
  4. 内核扩展:一些比较老牌的沙盒会采用在内核安装扩展来限制进程行为,并且都配有极其复杂的配置文档,启动过程也非常复杂,如 Sandboxie 和 judge0,他们都是内核的方案,但 judge0 就曾因为配置问题出现过一个及其严重的 CVE,并且它是牵一发动全身的,内核扩展要求特权容器,这导致了一旦绕过它的限制,docker 的限制也会变得没有意义。

我们感觉到目前的方案都不是非常契合 Dify 本身的业务,都有多多少少不能满足我们需要的地方,如运行太慢或者某个语言特点或者存在潜在的安全隐患,因此我们最后选择了自研一套方案,其大概的画像如下:
  1. 多层的隔离:理所当然的大家都会想到 Docker 容器,当然,我们也这么做了,这也可以给到用户十足的安全感,只是我们只在 Dify 启动阶段开启一个 sandbox 容器,它内部会维护一个 http 服务,接收来自 Dify 的代码执行请求,并不会每次有一个任务就新建一个 Docker 容器,不过这也限制系统在了 Linux 上,而在 Windows、Mac 等平台就需要 DockerDesktop、Orbstack 等工具。
  2. 系统层级的隔离:Linux 上的系统沙箱方案最常见的其实是 Docker,但是我们已经套了一层 Docker 了,因此我们需要再进一步,使用 Docker 的底层依赖的一项技术:Seccomp。
    可以将 Seccomp 看做是一个过滤器,它会过滤所有尝试访问系统的请求,包括但不限于读写文件、修改系统配置、网络访问,甚至是标准输入输出,都会经过 Seccomp,因为这些操作本质上都是一个个system call (syscall),一个 syscall 就是一次对系统的访问,其流程如下:

通过设置 Seccomp,我们就可以做到在一个进程尝试进行任何非法 syscall 时拦截其行为,最常见的,拦截文件访问、进程创建、磁盘 mount、系统修改。

然而,在不同的芯片架构上,有完全不同的 syscall 体系,并且 syscall 的数量非常庞大,如写文件的系统调用编号在 amd64 上是 2,但是在 arm64 上就是 64,amd64上有 300+系统调用,但是 arm64 上就有 400+,如果用黑名单的策略很有可能出现有哪个系统调用被意外放行,这会导致很严重的安全隐患,因此 DifySandbox 采用了白名单的策略,只放行必要的权限,所有非必要权限都进行拦截。

1. 在文件层面,我们还需要为 Sandbox 的子进程虚拟一套文件系统,将 Sandbox 宿主容器的文件系统和 Sandbox 中运行用户代码的进程的文件系统隔离开来,主要原因是 Seccomp 只能允许或禁止对所有文件的访问,如果粒度更细一些,就是需要一些文件可以被正常访问,如 Python 的第三方依赖,但是另一些文件不能被访问,如 /etc/passwd 等敏感文件,综上,我们需要隔离一个独立的文件系统出来,这就是 Linux 中的 chroot (change root),它可以更改一个进程的根目录到一个临时目录下,比如说我们在 Python 进程中执行了 chroot ("/tmp") 以后,这个 Python 进程再 ls / 看到的就都是原本 /tmp 下的文件,它可以有效隔离文件系统,如下图所示,每一个 sandbox 文件夹最后都会变成某一个 sandbox 进程的根目录。

但是 chroot 存在一些小的绕过问题,如果再次 chroot 到一个子文件夹下,就可以 cd .. 进入到外面本应该不能被访问的文件夹,但是 chroot 需要 root 权限,因此在进入到用户编写的代码逻辑之前,我们需要更改进程权限,使得进程的当前用户/组转移到非 root 用户/组上,还比如存在一些如 openat 这样的系统调用可以绕过的问题,不过也都会通过一些细节处理来防护,这里就不多展开赘述。

2. 网络层面,其实不太好直接在 Sandbox 内进行处理,首先因为docker本身限制了iptables,在没有 k8s 环境的情况下,我们很难配置系统层级的网络策略,再者网络配置是一个极其复杂的工程,并非一点简单的策略就可以解决的,我们需要给用户提供可以自由配置的办法,同时,在k8s和docker-compose中,网络隔离策略的配置也是完全不同的,综上所述,我们最终的解决方案是docker-compose和k8s各做一套:

  • docker-compose 上,我们使用一个独立的 internal 模式的网络来作为 sandbox 的网络,再引入一个 proxy 容器连接外部网络,作为 sandbox 的代理容器,最终在proxy容器上配置代理规则,就可以实现我们提到的自由配置,网络大概如下图所示,Sandbox 位于 SSRF_PROXY_NET 中,这个网络是一个内部网络,无法访问外部,同时 proxy 器也位于这个网络中,同时 proxy 容器还处于 DEFAULT 网络中,那么就可以由 proxy 作为代理访问外部网络,也可以配置很灵活的网络代理规则,从而确保内网安全,不过在这里 Dify 本身也使用了 proxy 容器,这是因为如 HTTP 节点之类的功能也存在安全问题,因此统一通过 proxy 的代理规则进行配置。

  • k8s 上就很简单了,只需要配置 Exgress 即可实现网络隔离,不多赘述。

总结

综上,DifySandbox 被设计为了一个依赖 Linux 本身而不依赖某一具体语言的沙盒运行时,主要的任务是给 Dify 提供一个安全的代码执行环境,确保 Dify 可以运行用户侧的代码,提供更灵活的功能,在安全层面,我们同时做了系统、磁盘、文件系统、网络、权限等多个方面的隔离策略以确保 Dify 的安全性,也做了多层的隔离,避免使用特权。

但是目前而言还是存在诸多不足的,如 Python、Nodejs 的依赖很难处理,即使目前已经有了一些 Python 的依赖手段,但仍然还是存在诸多问题没有解决,还比如因为白名单策略,导致 Python、Nodejs 本身的一些正常行为被意外拦截等等,不过我们也会进一步优化 Sandbox,给到大家更好的体验。

今天,我们开放了 DifySandbox 的源码,这是我们一直以来对开源的态度,希望为 DifySandbox 增加更多的可能性,也希望为社区带来更多的价值,让大家都能参与到 Dify 的建设中来,并且在未来的计划中,Sandbox 还有许多可以想象的空间,如实现图片处理、实现更灵活的数据分析,实现生成图片、视频等,届时也欢迎社区的大家一起参与讨论和建设~



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询