微信扫码
与创始人交个朋友
我要投稿
Hello,这里是 Dify 的后端工程师 Yeuoly,也是 DifySandbox 的作者。对于 Dify 的社区用户来说,应该对一个叫做 Sandbox 的 docker 服务不陌生,我们也收到过很多关于 Sandbox 的反馈,但是绝大多数用户对于 Sandbox 本身非常陌生,并不清楚 Sandbox 的内部细节,而这篇文章将会让你逐渐了解 Sandbox 内部究竟发生了什么。
DifySandbox:基于执行代码和系统安全需求
在 Dify 中,Workflow 是一项重要的能力,它允许用户以拖拉拽的方式来编排一个逻辑流,从而实现相对复杂的业务逻辑,在编排逻辑的过程中,复杂的数据处理是必不可少的,具体来说,我们可能有下面这些场景:
有时候我们需要处理 LLM 生成的 JSON 文本,从中提取结构化的数据,有时候又需要处理 HTTP 请求返回的 XML 文本、JSON 文本。
在另外一些场景下,甚至需要合并两个知识检索节点的输出内容,亦或者是合并知识检索节点 GoogleSearch 的结果。
上述的这些场景是多而杂的,但是究其根本仍然是数据处理,它们需要一个统一的解决方案,那么自然而然的我们就会想到写代码来实现,因为相比于数据处理节点这类高度定制化的实现,代码显然会更通用,如果给用户提供一个代码编辑框,在编辑框内,用户可以通过编写自己的代码来实现数据处理逻辑,那么这些问题就都迎刃而解。
既然需要在 Dify 中执行用户编写的代码,自然而然就需要考虑安全的问题,面对恶意用户的时候,代码执行就不再是一个正常功能,而变成了一个漏洞。
在理想情况下,用户编写的代码大多都是非恶意的代码,如果直接由 Dify 来执行代码,那么其实就是让 Dify 在服务器上创建一个新的 Python、Nodejs 进程,并将用户的代码提交到这个进程里去执行,就像下面这样,Process 就是执行代码的进程,User 需要首先访问 Dify,再由 Dify 来访问 Process。
而在恶意用户的手上,上面这个过程就会出现问题,Process 是直接运行在服务器上的,这会造成 Process 可以访问文件系统和数据库,那么此时就变成了下面这样了,用户编写的代码可以任意读取服务器上的文件,甚至在极端情况下可以读取或者删除整个 Dify 的数据库。
正是因为这样的问题,Dify 自研了 DifySandbox,它是一套代码沙箱方案,可以有效拦截恶意代码的操作,并放行正常业务逻辑,下面我们将详细介绍 Sandbox。
DifySandbox 的设计思路和实现机制
在设计之初,我们从几个角度考虑了 Sandbox 所需要满足的安全性能:
网络是一个老大难的问题,它的影响是方方面面的:
生产中很多的沙盒绕过都是通过网络,如 VMWare 虚拟机,它挺多的漏洞都是通过网络来实现的。
即使在代码层面、系统层面实现了防护,仍然阻止不了恶意代码通过网络来嗅探内网,从而非法访问内网资源,因此 Sandbox 还需要考虑如何隔离网络。
内核扩展:一些比较老牌的沙盒会采用在内核安装扩展来限制进程行为,并且都配有极其复杂的配置文档,启动过程也非常复杂,如 Sandboxie 和 judge0,他们都是内核的方案,但 judge0 就曾因为配置问题出现过一个及其严重的 CVE,并且它是牵一发动全身的,内核扩展要求特权容器,这导致了一旦绕过它的限制,docker 的限制也会变得没有意义。
通过设置 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+中大型企业
2024-07-11
2024-07-11
2024-07-09
2024-09-18
2024-06-11
2024-07-23
2024-07-20
2024-10-20
2024-07-12
2024-07-26