完整处理流程图

用户代码 安全沙箱 (add_seccomp.go) 预脚本 (prescript.py) 输出捕获 (output_capture.go) 执行器 (python/python.go) 服务层 (python.go) 控制器 (run.go) 并发控制 (cocrrent.go) 认证中间件 (auth.go) 路由层 (router.go) 客户端 用户代码 安全沙箱 (add_seccomp.go) 预脚本 (prescript.py) 输出捕获 (output_capture.go) 执行器 (python/python.go) 服务层 (python.go) 控制器 (run.go) 并发控制 (cocrrent.go) 认证中间件 (auth.go) 路由层 (router.go) 客户端 请求体: {language, code, preload, enable_network} alt [认证失败] alt [请求数超限] alt [工作线程超限] 1. 创建临时文件名 (UUID) 2. 生成 512-bit 随机密钥 3. XOR 加密用户代码 4. Base64 编码密钥和代码 5. 注入 prescript.py 模板 6. 写入临时文件 /tmp/xxx.py 命令: python3 /tmp/xxx.py [lib_path] [key] 1. 清空默认环境变量 2. 设置代理 (HTTP_PROXY/HTTPS_PROXY) 3. 设置允许的系统调用列表 白名单包括: - 基础系统调用 - 网络系统调用(如启用网络) - 或自定义系统调用列表 alt [退出码 != 0] [正常退出] alt [代码执行超时] [代码执行完成] 类似的流程... alt [language == "python3"] [language == "nodejs"] [不支持的语言] {code: 0/1, message: "success/error", data: {stdout, error}} POST /v1/sandbox/run 验证API Key 检查 X-Api-Key 头 401 Unauthorized 通过认证 检查并发请求数 (MaxRequest) 503 Too Many Requests 检查工作线程数 (MaxWorker) 等待可用线程 RunSandboxController 解析请求参数 根据 language 选择执行器 RunPython3Code(code, preload, options) 检查网络配置 设置超时时间 PythonRunner.Run() InitializeEnvironment() 创建 OutputCaptureRunner 设置超时定时器 创建 exec.Command 设置环境变量 CaptureOutput(cmd) 创建 stdout/stderr 管道 启动进程 加载 python.so 动态库 解码密钥和代码 执行 preload 代码 调用 DifySeccomp(uid, gid, enable_network) 1. chroot 到当前目录 2. 设置 no_new_privs 3. 加载系统调用白名单 4. 应用 Seccomp BPF 过滤器 5. setuid/setgid 降权到 nobody 沙箱环境就绪 XOR 解密用户代码 exec(code) 执行用户代码 输出结果到 stdout/stderr Kill 进程 返回超时错误 退出 读取 stdout/stderr 检查退出码 检查是否为 "bad system call" 返回错误信息 返回输出结果 收集 stdout/stderr 返回 DifySandboxResponse RunNodeJsCode() 400 Unsupported Language 200 OK + 执行结果

源码详细分析

服务启动阶段 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)

执行顺序关键点:

  1. Preload 在沙箱启动前执行 - 可能引入权限提升风险,需配置 <font style="color:#DF2A3F;">enable_preload: false</font>
  2. Seccomp 启动后,用户代码才执行 - 确保用户代码完全受限
  3. 代码解密在内存中进行 - 临时文件中不存在明文代码

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
  }
}

安全建议

  1. **必须关闭 **enable_preload - preload 在沙箱启动前执行,存在权限提升风险
  2. **限制 **max_workers - 防止资源耗尽
  3. 配置严格的 API Key - 防止未授权访问
  4. 部署 SSRF 代理 - 如果启用网络,必须配置代理防止 SSRF 攻击
  5. 定期更新依赖 - 配置 python_deps_update_interval
  6. 监控资源使用 - 配置 cgroups 限制 CPU/内存
  7. 不要自定义系统调用 - 除非完全理解其影响

相关文件索引

模块 文件路径 功能
入口 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 的安全设计包含:

  1. 应用层防护 - API 认证、并发控制、参数验证
  2. 数据加密 - XOR 加密代码,防止内存泄露
  3. 进程隔离 - Chroot、降权、环境变量清空
  4. 系统调用过滤 - Seccomp BPF 白名单
  5. 资源限制 - 超时控制、自动清理
  6. 网络隔离 - 代理转发、可选禁用

整体采用 纵深防御(Defense in Depth) 策略,即使某一层被突破,其他层仍能提供保护。

参考

一个轻量级、快速且安全的代码执行环境,支持多种编程语言:

https://github.com/langgenius/dify-sandbox

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐