1. 引言:潘多拉魔盒——当应用允许用户「写代码」
在 Dify 的 Workflow(工作流) 中,代码节点(Code Node) 是一个非常强大的功能,它允许用户直接在浏览器里编写 Python 或 JavaScript 代码。
但从服务端开发的视角来看,这就相当于把服务器的终端开放给了普通用户。如果没有隔离,用户可以在节点里写出这样的「毒药」代码:
💣 风险示例 1:窃取服务器机密
1 | # 用户在代码节点中悄悄输入以下代码 |
💣 风险示例 2:内存炸弹(OOM)
1 | # 简单的两行代码,瞬间吃光服务器内存,导致整个 Dify 宕机 |
如果这些代码直接在 Dify 的核心容器里裸奔,后果不堪设想。这就引出了我们今天的主角:隔离沙箱环境(Sandbox)。
2. 为什么需要隔离沙箱环境?
沙箱(Sandbox)就像是一个「防爆玻璃房」。把未知、不可信的用户代码放进去引爆,无论里面发生什么,都不会波及外面的宿主机。沙箱必须解决 三大核心诉求:
| 诉求 | 说明 | 对应防御对象 |
|---|---|---|
| 安全隔离(防黑客) | 哪怕执行 os.system("rm -rf /"),删掉的也只是沙箱内部的虚拟文件,宿主机安然无恙;断绝外部网络连接,防止刚才提到的密码窃取 |
越权访问、网络外联 |
| 资源限制(防滥用) | 严格控制 CPU 使用率和内存。刚才的「内存炸弹」一旦触发阈值,沙箱会直接将其「爆头」(OOM Kill) | OOM、死循环、CPU 抢占 |
| 环境纯粹性(防污染) | 每次执行都是全新的环境,张三执行的代码生成的临时文件,绝不会留在服务器上影响李四 | 残留状态污染、横向移动 |
3. 沙箱底层机制:如何把猛兽关进笼子?
在 Linux 体系下,沙箱并不是魔法,而是由一系列 内核级特性 组合而成的。我们可以将其理解为给进程套上「三层枷锁」。
3.1 第一层 · 障眼法:Namespace(命名空间)
让进程以为自己拥有一台独立的电脑。
进程看到的 PID、网络接口、挂载点都是「假的」,与宿主机完全隔离。
💡 运维命令演示
我们可以通过
unshare命令在 Linux 中手动体验创建一个隔离了进程(PID)和网络(NET)的沙箱:
1
2 # 启动一个全新的 bash,它看不到宿主机的进程,也没有网卡
unshare --pid --net --mount --fork /bin/bash
常见命名空间类型速查:
| Namespace | 隔离对象 | 攻击场景防护 |
|---|---|---|
CLONE_NEWPID |
进程 ID | 用户看不到宿主机进程,无法 kill 其他服务 |
CLONE_NEWNET |
网络栈 | 切断外联,阻断数据外传 |
CLONE_NEWNS |
挂载点 | 用户看不到宿主机真实文件系统 |
CLONE_NEWUTS |
主机名 / 域名 | 隐藏宿主身份 |
CLONE_NEWIPC |
进程间通信 | 切断共享内存 / 信号量通道 |
CLONE_NEWUSER |
用户 / 用户组 | 以非 root 身份运行,限制权限 |
3.2 第二层 · 紧箍咒:Cgroups(控制组)
由 Linux 内核提供,死死卡住进程的 硬件资源上限。比如限制某段代码最多只能用 50MB 内存,超过就立刻被内核杀死。
常见可控资源:cpu、memory、blkio(磁盘 IO)、pids(进程数上限)、net_cls(网络分类)。
3.3 第三层 · 防火墙:Seccomp(系统调用拦截)
代码即便在沙箱里,最终还是要向宿主机内核申请资源(比如打开文件 open())。Seccomp 是一份白名单,它规定沙箱内的代码只能调用 read、write 等无害操作。如果它敢调用 execve(尝试执行新木马程序),内核会直接报错。
三层关系:Namespace 管「能看到什么」,Cgroups 管「能用多少」,Seccomp 管「能做什么」。
4. 深入剖析:Dify 中是如何实现沙箱的?
Dify 作为一个生产级的 AI 框架,专门独立出了一个 dify-sandbox 微服务项目(基于 Go 语言编写)。它完美结合了上述 Linux 底层特性。
⚠️ 重要前提:沙箱不是万能的「黑盒」。它要求 执行环境具备充足的 Linux 权限(
CAP_SYS_ADMIN、CAP_SYS_PTRACE等)。在某些受限的 K8s 集群或 Mac/Windows 宿主机上,Namespace/Cgroups 可能无法生效,部署前需要确认。
4.1 微服务隔离架构
在部署层面,Dify 的核心 API(dify-api)和沙箱(dify-sandbox)是 完全解耦 的两个容器。
dify-api只负责工作流的流转;- 当走到代码节点时,
dify-api会将代码通过 HTTP 发给dify-sandbox执行; - 哪怕沙箱真的被极限黑客打挂了,Dify 的主业务依然存活。
本仓库中的相关定义位于 docker/docker-compose.yaml:955-977:
1 | # The DifySandbox |
调用关系图:
1 | ┌─────────────────────┐ HTTP /v1/sandbox/run ┌──────────────────────┐ |
双层网络防御:沙箱容器只挂在 ssrf_proxy_network(一个 internal: true 的内部网络,docker/docker-compose.yaml:1626-1630),物理上无法直达外网。所有出站请求都必须经过 ssrf_proxy 进行 SSRF 白名单过滤。
4.2 核心机制与关键代码解读
dify-sandbox 收到代码后,不会慢吞吞地去 docker run 创建新容器(那样延迟太高)。它通过 Go 原生的系统调用,直接拉起一个挂载了 Namespace 的受限子进程。
下面是高度浓缩的 Dify Sandbox 核心执行逻辑伪代码(基于 Go 原生特性),这是整个沙箱的「心脏」:
1 | // dify-sandbox 核心运行时伪代码 |
4.3 Dify 后端如何调用沙箱?
回到本仓库的 Python 代码侧,工作流执行到代码节点时,会通过 HTTP 调用 dify-sandbox。
入口:apicustom/core/helper/code_executor/code_executor.py:69-133
1 |
|
关键点解读:
code_execution_endpoint_url:从配置CODE_EXECUTION_ENDPOINT读取,默认指向http://sandbox:8194(即同 Compose 网络内的沙箱容器)。X-Api-Key头:携带沙箱 API Key(默认dify-sandbox),生产环境 必须 通过openssl rand -base64 42重新生成并写入.env。enable_network: True:是否允许沙箱内代码访问网络(取决于沙箱侧ENABLE_NETWORK与是否走ssrf_proxy代理)。- 三层超时:连接超时 / 读取超时 / 写入超时,任意一层超时就视为沙箱不可用。
配置侧(apicustom/configs/feature/init.py:103-126):
1 | class CodeExecutionSandboxConfig(BaseSettings): |
代码节点侧的输出校验(apicustom/.venv/lib/python3.12/site-packages/graphon/nodes/code/code_node.py:174-229):
沙箱返回结果后,CodeNode 还会基于 CodeNodeLimits(apicustom/.venv/lib/python3.12/site-packages/graphon/nodes/code/limits.py)做 应用层校验,作为最后一道防线:
- 字符串长度上限(
max_string_length) - 数字范围与精度(
max_number/min_number/max_precision) - 嵌套深度(
max_depth) - 数组长度上限(
max_number_array_length/max_string_array_length/max_object_array_length)
1 | def _check_string(self, value: str | None, variable: str) -> str | None: |
4.4 模板转换:参数如何注入?
Dify 的代码节点并不会把用户输入和代码「拼接」在一起,而是采用 预编译 + base64 注入 的方式,避免字符串拼接型注入风险。
Python3 转换器(apicustom/core/helper/code_executor/python3/python3_transformer.py):
1 | class Python3TemplateTransformer(TemplateTransformer): |
可以看到:
- 用户编写的
main(**inputs)代码通过占位符插入到 runner 顶部; - 工作流上游变量以 base64 编码 后以字符串占位符注入,由沙箱内代码
b64decode后再json.loads; - 返回结果用
<<RESULT>>...<<RESULT>>包裹,方便沙箱侧定位输出起点。
💡 这种「字符串拼接 + 沙箱执行」模式听起来很危险,但只要沙箱内核隔离足够强(即使用户在字符串里塞
__import__('os').system('rm -rf /'),在沙箱里也只能删掉沙箱内部文件),配合应用层OutputValidationError,就构成了端到端的安全闭环。
4.5 沙箱自身配置详解
docker/volumes/sandbox/conf/config.yaml 控制沙箱微服务本身的行为:
1 | app: |
⚠️
max_requests: 50是一个 状态隔离的工程技巧 —— 让 worker 处理 50 个请求后自杀重建,相当于「每次执行都是全新的环境」,彻底避免长期内存累积或残留状态污染。
4.6 兼顾开发体验:依赖库挂载
除了安全,Dify 还考虑了业务需求。纯粹的 Python 往往不够用,用户需要 numpy 或 requests 等。
Dify 的做法是:系统管理员可以在外部配置好 python-requirements.txt 并提前安装好,然后以 只读(Read-Only) 的形式挂载(Volume Mount)到沙箱的目录中。
1 | # docker-compose.yaml 中 sandbox 服务 |
优点:
- ✅ 运行时环境 不可被用户代码篡改;
- ✅ 沙箱重启后依赖依然存在;
- ✅ 业务侧可享受
numpy、pandas等强大的外部库能力。
5. 总结与开发启示
通过剖析 Dify 的沙箱机制,我们在日常的后端开发中可以吸取以下经验:
5.1 永远不要信任用户输入(Zero Trust)
只要业务涉及「动态代码执行」「动态 SQL 拼接」「动态 Shell 运行」,就必须 默认用户是恶意的,必须做物理或逻辑隔离。
SQL 场景的等价防御:使用参数化查询(Prepared Statement) + 最小权限账号;禁止字符串拼接 SQL。
5.2 防御性架构设计
高危模块(如代码执行模块)一定要 从单体应用中剥离出去,做微服务级别的解耦。不要让非核心功能的故障,拖垮整个核心交易链路。
参考 Dify 自身的部署:
1 | 核心业务 ── 隔离 ── 高危模块 ── 隔离 ── 外网(经代理白名单) |
5.3 拥抱云原生底层技术
了解 Linux 的 Namespace 和 Cgroups,不仅能帮助我们理解沙箱,更有助于我们深入理解 Docker 和 Kubernetes 的工作原理:
- Docker 容器 ≈ Namespace + Cgroups + UnionFS
- K8s Pod ≈ 一组共享 Network Namespace 的容器 + Cgroups 资源配额
- gVisor / Kata Containers ≈ 「沙箱中的沙箱」,连 syscall 都不直接打到宿主机内核
这是现代后端工程师的必修课。
5.4 沙箱不是银弹:常见误区
| 误区 | 真实情况 |
|---|---|
| 「上了沙箱就绝对安全」 | 沙箱依赖 Linux 内核权限;共享内核场景下内核漏洞可逃逸(Dirty COW、CVE-2022-0185 等) |
| 「开 enable_network 就完了」 | 必须配合 ssrf_proxy 域名白名单,否则沙箱可能被当作 SSRF 跳板 |
| 「默认 API Key 不用改」 | 生产环境必须替换为强随机密钥(openssl rand -base64 42) |
| 「沙箱挂了主业务不受影响」 | 业务流程高度依赖沙箱时(如代码节点串联)沙箱抖动会直接表现为业务失败,需要有降级 / 重试策略 |
6. 附录:本仓库中的关键代码索引
便于感兴趣的同事进一步深挖。
| 关注点 | 文件路径 | 关键行 |
|---|---|---|
| 沙箱服务定义 | docker/docker-compose.yaml | L955-L977 |
| 沙箱网络隔离 | docker/docker-compose.yaml | L1626-L1630 |
| SSRF 代理网络 | docker/docker-compose.yaml | L580-L590 |
| 沙箱配置文件 | docker/volumes/sandbox/conf/config.yaml | 全文 |
| 沙箱配置示例 | docker/volumes/sandbox/conf/config.yaml.example | 全文 |
| 后端沙箱配置 | apicustom/configs/feature/init.py | L103-L126 |
| 沙箱 HTTP 调用 | apicustom/core/helper/code_executor/code_executor.py | L19, L69-L133 |
| Python3 模板转换 | apicustom/core/helper/code_executor/python3/python3_transformer.py | 全文 |
| 代码节点输出校验 | apicustom/.venv/lib/python3.12/site-packages/graphon/nodes/code/code_node.py | L174-L229 |
| 节点输出阈值 | apicustom/.venv/lib/python3.12/site-packages/graphon/nodes/code/limits.py | 全文 |
| 代码节点异常类型 | apicustom/.venv/lib/python3.12/site-packages/graphon/nodes/code/exc.py | 全文 |
参考阅读: