智能体平台Dify的 Dify Sandbox 代码沙箱的请求处理流程
本文详细分析了Dify沙箱服务的完整处理流程和源码实现。服务启动阶段会初始化配置、安装依赖并启动HTTP服务器。路由层通过中间件实现API Key认证、请求数限制和并发控制。核心执行流程包括:代码加密、临时文件生成、沙箱环境初始化、系统调用白名单过滤、降权执行用户代码,并捕获输出结果。安全机制采用Seccomp BPF过滤器、chroot隔离和nobody用户降权,确保代码执行在严格受限的环境中。
完整处理流程图
源码详细分析
服务启动阶段 Run
**文件: cmd/server/main.go & **internal/server/server.go
// 启动流程
func Run() {
initConfig() // 初始化配置
go initDependencies() // 异步安装Python依赖
initServer() // 启动HTTP服务器
}
关键操作:
- 加载
conf/config.yaml配置文件 - 安装 Python 依赖包到沙箱环境
- 启动 Gin HTTP 服务器(默认端口 8194)
- 设置定时任务自动更新依赖包
路由和中间件层 Setup
**文件: **internal/controller/router.go
func Setup(Router *gin.Engine) {
PrivateGroup := Router.Group("/v1/sandbox/")
PrivateGroup.Use(middleware.Auth()) // API Key认证
InitRunRouter(PrivateGroup)
}
func InitRunRouter(Router *gin.RouterGroup) {
runRouter := Router.Group("")
runRouter.POST(
"run",
middleware.MaxRequest(config.MaxRequests), // 请求数限制
middleware.MaxWorker(config.MaxWorkers), // 并发限制
RunSandboxController,
)
}
中间件1: API Key 认证
**文件: **internal/middleware/auth.go
func Auth() gin.HandlerFunc {
config := static.GetDifySandboxGlobalConfigurations()
return func(c *gin.Context) {
if config.App.Key != c.GetHeader("X-Api-Key") {
c.AbortWithStatus(401)
return
}
}
}
中间件2: 并发控制
**文件: **internal/middleware/cocrrent.go
// MaxRequest - 限制最大请求数
func MaxRequest(max int) gin.HandlerFunc {
m := &MaxRequestIface{current: 0, lock: &sync.RWMutex{}}
return func(c *gin.Context) {
m.lock.RLock()
if m.current >= max {
m.lock.RUnlock()
c.JSON(503, types.ErrorResponse(-503, "Too many requests"))
c.Abort()
return
}
m.lock.RUnlock()
m.lock.Lock()
m.current++
m.lock.Unlock()
c.Next()
m.lock.Lock()
m.current--
m.lock.Unlock()
}
}
// MaxWorker - 限制最大并发工作线程
func MaxWorker(max int) gin.HandlerFunc {
sem := make(chan struct{}, max)
return func(c *gin.Context) {
sem <- struct{}{} // 获取令牌,满了会阻塞
defer func() { <-sem }() // 释放令牌
c.Next()
}
}
控制器层 RunSandboxController选择不同语言处理器
**文件: **internal/controller/run.go
func RunSandboxController(c *gin.Context) {
BindRequest(c, func(req struct {
Language string `json:"language" binding:"required"`
Code string `json:"code" binding:"required"`
Preload string `json:"preload"`
EnableNetwork bool `json:"enable_network"`
}) {
switch req.Language {
case "python3":
c.JSON(200, service.RunPython3Code(req.Code, req.Preload, &runner_types.RunnerOptions{
EnableNetwork: req.EnableNetwork,
}))
case "nodejs":
c.JSON(200, service.RunNodeJsCode(req.Code, req.Preload, &runner_types.RunnerOptions{
EnableNetwork: req.EnableNetwork,
}))
default:
c.JSON(400, types.ErrorResponse(-400, "unsupported language"))
}
})
}
服务层 RunPython3Code
**文件: **internal/service/python.go
func RunPython3Code(code string, preload string, options *runner_types.RunnerOptions) *types.DifySandboxResponse {
// 1. 检查选项配置
if err := checkOptions(options); err != nil {
return types.ErrorResponse(-400, err.Error())
}
// 2. 检查是否启用 preload(防止注入攻击)
if !static.GetDifySandboxGlobalConfigurations().EnablePreload {
preload = ""
}
// 3. 设置超时时间
timeout := time.Duration(
static.GetDifySandboxGlobalConfigurations().WorkerTimeout * int(time.Second),
)
// ⭐⭐⭐4. 创建并运行 Python 执行器
runner := python.PythonRunner{}
stdout, stderr, done, err := runner.Run(code, timeout, nil, preload, options)
if err != nil {
return types.ErrorResponse(-500, err.Error())
}
// 5. 收集输出(通过 channel)
stdout_str := ""
stderr_str := ""
defer close(done)
defer close(stdout)
defer close(stderr)
for {
select {
case <-done:
return types.SuccessResponse(&RunCodeResponse{
Stdout: stdout_str,
Stderr: stderr_str,
})
case out := <-stdout:
stdout_str += string(out)
case err := <-stderr:
stderr_str += string(err)
}
}
}
!执行器:代码加密与环境初始化 InitializeEnvironment
**文件: **internal/core/runner/python/python.go
func (p *PythonRunner) InitializeEnvironment(code string, preload string, options *types.RunnerOptions) (string, string, error) {
// 1. 生成随机文件名
temp_code_name := strings.ReplaceAll(uuid.New().String(), "-", "_")
// 2. 加载 prescript.py 模板
script := strings.Replace(
string(sandbox_fs),
"{{uid}}", strconv.Itoa(static.SANDBOX_USER_UID), 1,
)
script = strings.Replace(script, "{{gid}}", strconv.Itoa(static.SANDBOX_GROUP_ID), 1)
script = strings.Replace(script, "{{enable_network}}", "1/0", 1)
script = strings.Replace(script, "{{preload}}", preload, 1)
// 3. 生成 512-bit 随机密钥
key_len := 64
key := make([]byte, key_len)
rand.Read(key)
// 4. XOR 加密用户代码
encrypted_code := make([]byte, len(code))
for i := 0; i < len(code); i++ {
encrypted_code[i] = code[i] ^ key[i%key_len]
}
// 5. Base64 编码
code = base64.StdEncoding.EncodeToString(encrypted_code)
encoded_key := base64.StdEncoding.EncodeToString(key)
// 6. 注入加密代码到模板,并重命名为code
code = strings.Replace(script, "{{code}}", code, 1)
// 7. 将模板写入临时文件,并返回模板路径和解密key
untrusted_code_path := fmt.Sprintf("%s/tmp/%s.py", LIB_PATH, temp_code_name)
os.WriteFile(untrusted_code_path, []byte(code), 0755)
return untrusted_code_path, encoded_key, nil
}
关键安全措施:
- 使用 XOR 加密代码,防止内存中明文暴露
- 512-bit 随机密钥,每次执行不同
- 代码以 Base64 编码传递,避免注入攻击
- 执行后自动删除临时文件
!执行器:进程创建与资源限制 Run
**文件: **internal/core/runner/python/python.go
func (p *PythonRunner) Run(...) (chan []byte, chan []byte, chan bool, error) {
// ⭐⭐⭐⭐创建命令
cmd := exec.Command(
configuration.PythonPath, // /usr/local/bin/python3
untrusted_code_path, // /tmp/xxx.py
LIB_PATH, // 依赖库路径
key, // 解密密钥
)
// 清空环境变量(安全措施)
cmd.Env = []string{}
cmd.Dir = LIB_PATH
// 设置代理(如果启用网络)
if configuration.Proxy.Socks5 != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("HTTPS_PROXY=%s", configuration.Proxy.Socks5))
cmd.Env = append(cmd.Env, fmt.Sprintf("HTTP_PROXY=%s", configuration.Proxy.Socks5))
}
// 设置允许的系统调用
if len(configuration.AllowedSyscalls) > 0 {
cmd.Env = append(cmd.Env,
fmt.Sprintf("ALLOWED_SYSCALLS=%s", strings.Join(...)),
)
}
// 启动输出捕获
output_handler := runner.NewOutputCaptureRunner()
output_handler.SetTimeout(timeout)
output_handler.SetAfterExitHook(func() {
os.Remove(untrusted_code_path) // 清理临时文件
})
// ⭐⭐
err = output_handler.CaptureOutput(cmd)
return output_handler.GetStdout(), output_handler.GetStderr(), output_handler.GetDone(), nil
}
输出捕获与超时控制 CaptureOutput
**文件: **internal/core/runner/output_capture.go
func (s *OutputCaptureRunner) CaptureOutput(cmd *exec.Cmd) error {
// 1. 设置超时定时器
timeout := s.timeout
if timeout == 0 {
timeout = 5 * time.Second
}
timer := time.AfterFunc(timeout, func() {
if cmd != nil && cmd.Process != nil {
s.WriteError([]byte("error: timeout\n"))
cmd.Process.Kill() // 强制杀死进程
}
})
// 2. 创建管道
stdout_reader, _ := cmd.StdoutPipe()
stderr_reader, _ := cmd.StderrPipe()
// 3. ⭐⭐⭐启动进程
cmd.Start()
wg := sync.WaitGroup{}
wg.Add(2)
// 4. 异步读取 stdout
go func() {
defer wg.Done()
for {
buf := make([]byte, 1024)
n, err := stdout_reader.Read(buf)
if err == io.EOF {
break
}
s.WriteOutput(buf[:n]) // 发送到 channel
}
}()
// 5. 异步读取 stderr
go func() {
defer wg.Done()
for {
buf := make([]byte, 1024)
n, err := stderr_reader.Read(buf)
if err == io.EOF {
break
}
s.WriteError(buf[:n]) // 发送到 channel
}
}()
// 6. 等待进程结束
go func() {
wg.Wait()
status, err := cmd.Process.Wait()
if err != nil {
s.WriteError([]byte(fmt.Sprintf("error: %v\n", err)))
} else if status.ExitCode() != 0 {
exit_string := status.String()
if strings.Contains(exit_string, "bad system call") {
s.WriteError([]byte("error: operation not permitted\n"))
} else {
s.WriteError([]byte(fmt.Sprintf("error: %v\n", exit_string)))
}
}
if s.after_exit_hook != nil {
s.after_exit_hook() // 清理临时文件
}
timer.Stop()
s.done <- true // 通知完成
}()
return nil
}
Python 预脚本(沙箱入口) prescript.py
cmd.Start()启动进程会运行该脚本:
**文件: **internal/core/runner/python/prescript.py
import ctypes
import os
import sys
from base64 import b64decode
# 1. 加载动态库(包含 Seccomp 实现)
lib = ctypes.CDLL("./python.so")
lib.DifySeccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
lib.DifySeccomp.restype = None
# 2. 获取运行路径和解密密钥(从命令行参数)
running_path = sys.argv[1] # /path/to/lib
key = b64decode(sys.argv[2])
os.chdir(running_path)
# 3. 执行 preload 代码(注入点,需严格控制)
{{preload}}
# 4. ⭐⭐⭐⭐ 启动 Seccomp 沙箱
lib.DifySeccomp({{uid}}, {{gid}}, {{enable_network}})
# 5. 解密并执行用户代码
code = b64decode("{{code}}")
def decrypt(code, key):
key_len = len(key)
code_len = len(code)
code = bytearray(code)
for i in range(code_len):
code[i] = code[i] ^ key[i % key_len]
return bytes(code)
code = decrypt(code, key)
exec(code)
执行顺序关键点:
- Preload 在沙箱启动前执行 - 可能引入权限提升风险,需配置
<font style="color:#DF2A3F;">enable_preload: false</font> - Seccomp 启动后,用户代码才执行 - 确保用户代码完全受限
- 代码解密在内存中进行 - 临时文件中不存在明文代码
Seccomp 沙箱 InitSeccomp(最核心安全机制)
**文件: **internal/core/lib/python/add_seccomp.go
func InitSeccomp(uid int, gid int, enable_network bool) error {
// 1. Chroot 到当前目录(文件系统隔离)
err := syscall.Chroot(".")
if err != nil {
return err
}
err = syscall.Chdir("/")
// 2. 禁止提升权限
lib.SetNoNewPrivs()
// 3. 加载系统调用白名单
allowed_syscalls := []int{}
allowed_not_kill_syscalls := []int{}
allowed_not_kill_syscalls = append(allowed_not_kill_syscalls,
python_syscall.ALLOW_ERROR_SYSCALLS...)
// 从环境变量读取自定义系统调用(如果有)
allowed_syscall := os.Getenv("ALLOWED_SYSCALLS")
if allowed_syscall != "" {
nums := strings.Split(allowed_syscall, ",")
for num := range nums {
syscall, _ := strconv.Atoi(nums[num])
allowed_syscalls = append(allowed_syscalls, syscall)
}
} else {
// 使用默认白名单
allowed_syscalls = append(allowed_syscalls, python_syscall.ALLOW_SYSCALLS...)
if enable_network {
allowed_syscalls = append(allowed_syscalls, python_syscall.ALLOW_NETWORK_SYSCALLS...)
}
}
// 4. 应用 Seccomp BPF 过滤器
err = lib.Seccomp(allowed_syscalls, allowed_not_kill_syscalls)
if err != nil {
return err
}
// 5. 降权到 nobody 用户
err = syscall.Setuid(uid)
if err != nil {
return err
}
err = syscall.Setgid(gid)
if err != nil {
return err
}
return nil
}
安全措施详解:
| 机制 | 说明 | 防护能力 |
|---|---|---|
| Chroot | 将根目录限制在 /path/to/lib,无法访问系统其他文件 |
防止读取敏感文件 |
| SetNoNewPrivs | 禁止通过 execve() 获取更多权限 |
防止权限提升 |
| Seccomp BPF | 只允许白名单内的系统调用,其他直接杀死进程 | 防止系统调用攻击 |
| Setuid/Setgid | 降权到 nobody (UID 65534) |
最小权限原则 |
安全机制总结
多层防护体系

关键技术点
代码加密传递(XOR 运算)
# Go 端加密
encrypted_code[i] = code[i] ^ key[i%64]
# Python 端解密
code[i] = code[i] ^ key[i % 64]
为什么不用 AES 等标准加密?
- 性能 - XOR 运算极快,无需依赖外部库
- 目的 - 防止内存中明文泄露,而非传输加密
- 密钥管理 - 每次执行生成新密钥,通过命令行参数传递
Seccomp 系统调用白名单
**文件: **internal/static/python_syscall/syscalls_amd64.go
默认允许的系统调用(节选):
var ALLOW_SYSCALLS = []int{
syscall.SYS_READ,
syscall.SYS_WRITE,
syscall.SYS_OPEN,
syscall.SYS_CLOSE,
syscall.SYS_MMAP,
syscall.SYS_MUNMAP,
syscall.SYS_BRK,
syscall.SYS_RT_SIGACTION,
syscall.SYS_IOCTL,
syscall.SYS_GETPID,
syscall.SYS_GETTIMEOFDAY,
syscall.SYS_CLOCK_GETTIME,
// ... 约 60+ 系统调用
}
var ALLOW_NETWORK_SYSCALLS = []int{
syscall.SYS_SOCKET,
syscall.SYS_CONNECT,
syscall.SYS_SENDTO,
syscall.SYS_RECVFROM,
// ... 网络相关系统调用
}
明确禁止的危险操作:
- ❌
execve- 无法执行其他程序 - ❌
fork- 无法创建子进程 - ❌
kill- 无法杀死其他进程 - ❌
ptrace- 无法调试其他进程 - ❌
mount- 无法挂载文件系统
Chroot 文件系统隔离
真实文件系统:
/
├── etc/
├── home/
├── usr/
└── var/
Chroot 后用户看到的:
/ (实际是 /path/to/dify-sandbox/lib)
├── python3.x/
├── dependencies/
├── tmp/
└── python.so
用户代码无法访问:
- ❌
/etc/passwd - ❌
/home/user/.ssh/ - ❌
/var/log/
配置文件说明
**文件: **conf/config.yaml
app:
port: 8194
debug: false
key: "dify-sandbox" # API Key
max_workers: 4 # 最大并发工作线程
max_requests: 100 # 最大请求队列
worker_timeout: 15 # 执行超时(秒)
python_path: "/usr/local/bin/python3"
enable_network: false # 全局网络开关
enable_preload: false # 🔥 建议关闭,防止注入攻击
proxy:
http: "http://ssrf_proxy:3128"
https: "http://ssrf_proxy:3128"
allowed_syscalls: [] # 自定义系统调用(留空使用默认白名单)
完整执行流程示例
请求示例
curl -X POST http://localhost:8194/v1/sandbox/run \
-H "X-Api-Key: dify-sandbox" \
-H "Content-Type: application/json" \
-d '{
"language": "python3",
"code": "print(\"Hello Dify\")",
"preload": "",
"enable_network": false
}'
!!!内部执行流程
1. 请求到达 -> Gin Router
2. Auth 中间件 -> 验证 X-Api-Key
3. MaxRequest 中间件 -> 检查请求数 (current < 100)
4. MaxWorker 中间件 -> 获取工作线程令牌 (< 4)
5. RunSandboxController -> 解析请求参数
6. Service.RunPython3Code -> 检查配置,设置超时 15s
7. PythonRunner.InitializeEnvironment:
- 生成随机文件名: a1b2c3d4_e5f6_7890_...
- 生成 512-bit 密钥: [random 64 bytes]
- XOR 加密代码: "print(...)" -> [encrypted bytes]
- Base64 编码: "SGVsbG8gRGlmeQ=="
- 写入文件: /tmp/a1b2c3d4_e5f6_7890.py
8. PythonRunner.Run:
- 创建命令: python3 /tmp/xxx.py /lib/path [key]
- 设置环境变量: ALLOWED_SYSCALLS="0,1,2,3,..."
- 启动进程
9. prescript.py 启动:
- 加载 python.so
- 解码密钥
- 执行 preload (空)
- 🔒 调用 DifySeccomp(65534, 65534, False)
10. add_seccomp.go:
- chroot(".")
- SetNoNewPrivs()
- Seccomp([0,1,2,3,...], [])
- setuid(65534)
- setgid(65534)
11. prescript.py 继续:
- 解密代码: XOR 解密 -> "print(\"Hello Dify\")"
- 执行: exec(code)
12. 用户代码执行:
- print() -> stdout
13. OutputCaptureRunner:
- 读取 stdout: "Hello Dify\n"
- 读取 stderr: ""
- 进程退出,exit code: 0
- 调用 after_exit_hook -> 删除 /tmp/xxx.py
- 发送 done 信号
14. Service 收集输出:
- stdout_str = "Hello Dify\n"
- stderr_str = ""
15. 返回响应:
{
"code": 0,
"message": "success",
"data": {
"stdout": "Hello Dify\n",
"error": null
}
}
安全建议
- **必须关闭 **
enable_preload- preload 在沙箱启动前执行,存在权限提升风险 - **限制 **
max_workers- 防止资源耗尽 - 配置严格的 API Key - 防止未授权访问
- 部署 SSRF 代理 - 如果启用网络,必须配置代理防止 SSRF 攻击
- 定期更新依赖 - 配置
python_deps_update_interval - 监控资源使用 - 配置 cgroups 限制 CPU/内存
- 不要自定义系统调用 - 除非完全理解其影响
相关文件索引
| 模块 | 文件路径 | 功能 |
|---|---|---|
| 入口 | cmd/server/main.go |
服务启动 |
| 服务器 | internal/server/server.go |
HTTP 服务器初始化 |
| 路由 | internal/controller/router.go |
路由配置 |
| 控制器 | internal/controller/run.go |
请求处理 |
| 认证 | internal/middleware/auth.go |
API Key 认证 |
| 并发控制 | internal/middleware/cocrrent.go |
请求/线程限制 |
| 服务层 | internal/service/python.go |
Python 执行服务 |
| 运行器 | internal/core/runner/python/python.go |
Python 执行器 |
| 输出捕获 | internal/core/runner/output_capture.go |
进程输出捕获 |
| 预脚本 | internal/core/runner/python/prescript.py |
沙箱入口脚本 |
| Seccomp | internal/core/lib/python/add_seccomp.go |
系统调用过滤 |
| 系统调用表 | internal/static/python_syscall/syscalls_amd64.go |
白名单定义 |
| 配置 | conf/config.yaml |
全局配置 |
总结
Dify Sandbox 的安全设计包含:
- 应用层防护 - API 认证、并发控制、参数验证
- 数据加密 - XOR 加密代码,防止内存泄露
- 进程隔离 - Chroot、降权、环境变量清空
- 系统调用过滤 - Seccomp BPF 白名单
- 资源限制 - 超时控制、自动清理
- 网络隔离 - 代理转发、可选禁用
整体采用 纵深防御(Defense in Depth) 策略,即使某一层被突破,其他层仍能提供保护。
参考
一个轻量级、快速且安全的代码执行环境,支持多种编程语言:
更多推荐


所有评论(0)