一、代码沙箱概述
代码沙箱(Code Sandbox) 是一个隔离的代码执行环境,允许在受控的环境中安全地运行不信任的代码。它的核心目标是在提供代码执行功能的同时,防止恶意或有缺陷的代码对系统造成危害。
在 Dify 中,代码沙箱主要用于以下场景:
- 工作流代码节点:用户可以在工作流中添加代码节点,用 Python 或 JavaScript 处理工作流中的数据
- 代码执行工具:作为智能体的工具,大模型可以自主调用代码执行器完成计算任务
- 模板转换:在数据处理过程中,使用代码对数据进行转换和清洗
设计一个安全的代码沙箱需要满足以下需求:

二、Dify 为什么需要自研 Sandbox?
1、为 Dify Workflow 提供代码执行环境
DifySandbox 与 Workflow 深度集成,为 Code 节点、Template Transform 节点、LLM 节点以及 Tool 节点中的 Code Interpreter 提供底层代码执行环境。作为复杂 Workflow 的运行基础,DifySandbox 能够支持 LLM 应用内部的结构化数据转换、数据拼接和逻辑处理,帮助开发者构建更加强大的应用。
2、阻拦恶意代码
单一的代码执行环境无法提供足够的安全保障。没有适当的限制,正常的业务逻辑和潜在的恶意代码都能在环境中执行。恶意代码可能会访问敏感资源、读取服务器文件,甚至删除数据库。针对这一安全隐患,DifySandbox 实现了双重功能:既能隔离潜在的恶意代码,又能让正常的业务代码顺利运行,从而提供了一个真正安全的代码执行环境。
3、现有沙盒方案的局限性
通过对目前市面上的主流沙盒方案的调研,我们发现市面上现有的主流方案存在很多局限性:
-
WebAssembly :这是一个目前大多数沙盒的方案,通过将 Python、Nodejs 的解释器编译为一个 WebAssembly 的运行时,从而可以在 Nodejs 中或者浏览器中运行 Python 代码,在确保规范使用 WebAssembly 的情况下,这个方案在系统层面是非常安全的,但是它带来的问题就是自由度不够高,比如说如果需要安装第三方依赖困难,那么这样做起来就会有非常多像架构不兼容这样的深坑,并且要同时为 Python 和 Nodejs 做不同的处理,并不是一个特别灵活的方案。
-
Docker :基于一次执行一个容器的方案运行速度慢,并可能导致 docker 被劫持。
-
特定语言的沙盒包 :限制在单一语言,不是通用方案。Nodejs 下有 vm2,Python 下有 Pypy,但是他们都限制在了某一个语言上,并且都有各自的限制,并不是一个通用方案,并且他们在处理依赖的时候也需要按照其规范处理,以及像 Pypy 的 Python 版本也有严格的限制,依赖也不是那么得方便,当然这里还存在一些其他的库,如 pyodide,不过最大的问题还是 Nodejs 和 Python 的方案不通用,维护起来有一定难度。
-
内核扩展 :配置复杂,并要求系统特权,存在潜在安全隐患。一些比较老牌的沙盒会采用在内核安装扩展来限制进程行为,并且都配有极其复杂的配置文档,启动过程也非常复杂,如 Sandboxie 和 judge0,他们都是内核的方案,但 judge0 就曾因为配置问题出现过一个及其严重的 CVE,并且它是牵一发动全身的,内核扩展要求特权容器,这导致了一旦绕过它的限制,docker 的限制也会变得没有意义。
三、DifySandbox 的安全机制
为了突破传统沙盒方案的瓶颈,提供更安全,灵活,通用的代码执行环境,自主研发了 DifySandbox,同时设计了操作系统、文件系统、网络等多个方面的隔离策略来确保 Dify 的安全性:
-
系统安全 :使用白名单策略,只允许运行特定的系统调用,有效防止意外或恶意的系统调用绕过,同时保证了业务代码的正常运行。
-
文件系统隔离:用户代码将运行在一个独立的隔离的文件系统中。
-
网络隔离:独立的 Sandbox 网络,使用代理容器进行网络访问,确保内网系统的安全,同时提供了灵活的代理配置方案。
-
权限隔离:确保恶意代码永远保持在最低权限
多层的隔离 :理所当然的大家都会想到 Docker 容器,当然,我们也这么做了,这也可以给到用户十足的安全感,只是我们只在 Dify 启动阶段开启一个 sandbox 容器,它内部会维护一个 http 服务,接收来自 Dify 的代码执行请求,并不会每次有一个任务就新建一个 Docker 容器,不过这也限制系统在了 Linux 上,而在 Windows、Mac 等平台就需要 DockerDesktop、Orbstack 等工具。
系统层级的隔离 :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 提供了以下几方面的优势:
-
可扩展性 :DifySandbox 是基于 Seccomp 的系统层级解决方案,已支持 Python 和 NodeJS,并具备扩展至更多编程语言的技术基础。
-
高性能 :作为系统级解决方案,DifySandbox 采用单容器多任务运行模式,避免了为每个任务单独创建容器的资源消耗,从而显著提升了任务处理速度。
-
开放使用:DifySandbox 不仅服务于 Dify,还是一个独立的开源项目。它为所有需要安全执行代码的开发者提供了自由部署和免费使用的机会。
四、代码执行的实现

代码执行相关的逻辑,其实现位于 CodeExecutor 类:


其中 execute_workflow_code_template() 函数负责工作流中的代码节点的执行,注意它并没有直接执行用户的代码,而是做了一层模板转换。因为代码执行服务是通过标准输出获取执行结果的,而工作流的代码节点中用户输入的代码必须包含一个 main() 入口:
python
def main(x: int, y: int) -> dict:
return {
'sum' : x + y
}
如果直接执行不会有任何输出,所以 Dify 在调用代码执行服务之前,先使用一段预置的代码模板将用户代码和输入参数包起来。模板如下:

最终的输出结果会被转换为 JSON 格式并通过 print 打印出来,打印的时候加上 <<RESULT>> 这个特殊标签,防止 main 函数里其他的 print 对结果造成干扰,方便解析。
另外,CodeExecutor 中还有一个 execute_code() 函数,这才是真正的代码执行入口,支持 Python 或 JavaScript 两种编程语言,它负责将代码发送到代码沙箱服务并处理返回结果:
bash
$ curl -X POST http://127.0.0.1:8194/v1/sandbox/run \
-H "X-Api-Key: dify-sandbox" \
-H "Content-Type: application/json" \
-d '{
"language": "python3",
"code": "print('"'"'hello'"'"')",
"preload": "",
"enable_network": true
}'

该接口通过 X-Api-Key 头支持简单的鉴权,该值可以在 .env 文件中修改:

五、Dify 代码沙箱架构
Dify 的代码沙箱服务以 Docker 容器运行,采用了 防御纵深(Defense in Depth) 的设计思想,实现了多层安全防御,即使某一层防御被突破,其他层仍然能够保护系统。其执行流程如下

整个过程涉及多个安全层。
第一层:代码加密与编码
Dify 的代码沙箱在执行用户代码时,首先会将用户代码写到一个临时文件中,然后再启动 Python 或 Node.js 去运行该脚本文件,通过捕获 Python 或 Node.js 进程的 stdout 和 stderr 获取代码执行结果。
为了防止用户的代码以明文形式出现在磁盘上,Dify 在写入临时文件时对代码做了一次简单的加密:

用户的代码也不是直接执行的,而是通过另一个模板文件 prescript.py 动态生成的,可以在这个模板文件中看到用户代码的解密过程:

虽然 XOR 加密的安全性相对较弱,但由于这个密钥是一次性的,考虑到所有操作都在容器中,以及配合其他安全机制,这种方案在实践中是足够的。
第二层:进程隔离
每次代码执行都会创建一个独立的子进程,用户代码在这个进程中运行,与沙箱服务的主进程完全隔离。这样,即使代码因某种原因崩溃或消耗过多资源,也不会影响沙箱服务本身。另外,参考 docker/volumes/sandbox/conf/config.yaml 文件,沙箱还支持配置以下资源限制:

|----------------|-------------|---------|
| 配置项 | 说明 | 默认值 |
| max_workers | 最大并发执行进程数 | 4 |
| max_requests | 请求队列大小 | 50 |
| worker_timeout | 单个代码执行的超时时间 | 5s |
超过超时时间的进程会被强制杀死,这是一个重要的资源保护机制。
第三层:文件系统隔离
chroot是 Linux 提供的一个系统调用,可以改变进程的根目录 。当一个进程执行 chroot("/some/path") 后,对于这个进程来说,/some/path 就变成了新的根目录,进程无法访问此目录之外的任何文件。
在 Dify 沙箱中,Python 代码执行时会被 chroot 到一个特殊的沙箱目录,如 /var/sandbox/sandbox-python 目录。这个目录包含了 Python 运行时所需的最小文件集合。这样,即使用户代码尝试执行 open("/etc/passwd"),它实际上会尝试打开 /var/sandbox/sandbox-python/etc/passwd,而这个文件并不存在,因此访问会被拒绝。
这个最小化的文件系统被称为 chroot 监狱,即使用户代码知道绝对路径,也无法访问这个监狱之外的文件。
Dify 沙箱通过 Go 语言的 syscall.Chroot() 系统调用实现该功能:

需要注意的是,chroot 本身并不是一个强大的安全机制,如果进程有能力调用 chdir() 和其他系统调用,可能找到逃逸的方式,需要配合其他机制来形成完整的防护。因此,Dify 在应用 chroot 的同时,还使用了 seccomp 来限制进程可以调用的系统调用。
第四层:用户权限隔离
沙箱中的代码不应该以 root 身份运行。Dify 在代码执行时,会使用 setuid 和 setgid 系统调用,将进程的用户身份和组身份切换到一个非特权用户:

这个非特权用户名为 sandbox,ID 为 65537,是由沙箱服务在启动时自动创建的。
这样做的好处是:
-
限制文件和目录的访问权限(基于文件的 Unix 权限位)
-
防止进程获得 root 权限进行的操作
-
减小代码逃逸后的影响范围
除了权限降级外,Dify 还使用了prctl(PR_SET_NO_NEW_PRIVS) 系统调用来禁止进程及其所有子进程获得新的权限:
六、Dify沙盒安装panda
问题:代码执行节点使用pandas报错: ModuleNotFoundError: No module named 'pandas' error: exit status 255

在dify/docker/volumes/sandbox/dependencies/python-requirements.txt文件中加入pandas库:

建议沙盒内配置pip源:

修改docker-compose.yml中有关sandbox的部分:

执行如下指令:
bash
docker-compose -f docker-compose.yml down
docker-compose -f docker-compose.yml up -d
sandbox容器日志显示pandas库已安装:

再次对代码执行节点执行,报错:error: operation not permitted

原因: sandbox为了安全,里面的代码被赋予了极其有限的权限,基本只能进行一些不涉及系统文件读写、不依赖特殊外部库的简单计算和网络请求。
解决方法 :放开权限,修改dify/docker/volumes/sandbox/conf/config.yaml,allowed_syscalls选项定义了沙盒中的程序可以向操作系统发出的所有"系统调用"许可。最直接、最彻底的解决方法就是将从0到499这全部五百个左右的系统调用变量全部添加,这相当于允许沙盒执行操作系统层面上的几乎所有操作。

重启sandbox服务,再次执行代码执行节点,成功:

七、参考
Dify 宣布开源 DifySandbox - 快速、安全的代码运行环境 - 53AI-AI知识库|企业AI知识库|大模型知识库|AIHub
DifySandbox 的构建背景和实现机制 - 53AI-AI知识库|企业AI知识库|大模型知识库|AIHub
【容器安全系列Ⅵ】- Linux seccomp隔离-腾讯云开发者社区-腾讯云
dify中的沙盒sandbox介绍_dify sandbox-CSDN博客