一、代码沙箱概述代码沙箱Code Sandbox 是一个隔离的代码执行环境允许在受控的环境中安全地运行不信任的代码。它的核心目标是在提供代码执行功能的同时防止恶意或有缺陷的代码对系统造成危害。在 Dify 中代码沙箱主要用于以下场景工作流代码节点用户可以在工作流中添加代码节点用 Python 或 JavaScript 处理工作流中的数据代码执行工具作为智能体的工具大模型可以自主调用代码执行器完成计算任务模板转换在数据处理过程中使用代码对数据进行转换和清洗设计一个安全的代码沙箱需要满足以下需求二、Dify 为什么需要自研 Sandbox1、为 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 下有 vm2Python 下有 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 上就是 64amd64上有 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() 入口def main(x: int, y: int) - dict: return { sum : x y }如果直接执行不会有任何输出所以 Dify 在调用代码执行服务之前先使用一段预置的代码模板将用户代码和输入参数包起来。模板如下最终的输出结果会被转换为 JSON 格式并通过 print 打印出来打印的时候加上 RESULT 这个特殊标签防止 main 函数里其他的 print 对结果造成干扰方便解析。另外CodeExecutor 中还有一个execute_code()函数这才是真正的代码执行入口支持 Python 或 JavaScript 两种编程语言它负责将代码发送到代码沙箱服务并处理返回结果$ 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最大并发执行进程数4max_requests请求队列大小50worker_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 系统调用将进程的用户身份和组身份切换到一个非特权用户这个非特权用户名为 sandboxID 为 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的部分执行如下指令docker-compose -f docker-compose.yml down docker-compose -f docker-compose.yml up -dsandbox容器日志显示pandas库已安装再次对代码执行节点执行报错error: operation not permitted原因sandbox为了安全里面的代码被赋予了极其有限的权限基本只能进行一些不涉及系统文件读写、不依赖特殊外部库的简单计算和网络请求。解决方法放开权限修改dify/docker/volumes/sandbox/conf/config.yamlallowed_syscalls选项定义了沙盒中的程序可以向操作系统发出的所有“系统调用”许可。最直接、最彻底的解决方法就是将从0到499这全部五百个左右的系统调用变量全部添加这相当于允许沙盒执行操作系统层面上的几乎所有操作。重启sandbox服务再次执行代码执行节点成功七、参考GitHub - langgenius/dify-sandbox: A lightweight, fast, and secure code execution environment that supports multiple programming languages · GitHubDify 宣布开源 DifySandbox - 快速、安全的代码运行环境 - 53AI-AI知识库|企业AI知识库|大模型知识库|AIHubDifySandbox 的构建背景和实现机制 - 53AI-AI知识库|企业AI知识库|大模型知识库|AIHub【容器安全系列Ⅵ】- Linux seccomp隔离-腾讯云开发者社区-腾讯云dify中的沙盒sandbox介绍_dify sandbox-CSDN博客学习 Dify 的代码沙箱 - 知乎如何在Dify沙盒中安装运行pandas、numpy_dify pandas-CSDN博客突破DIFY沙箱限制高效处理大文件_dify文档提取器,大文档卡死-CSDN博客