👋 大家好,我是十三!
最近在研究研发提效,和同事聊到了低代码平台,而这其中一个较难实现的部分就是“代码块”节点:用户在前端输入一段代码,服务端就能动态执行并返回结果,这要怎么实现代码块的执行以及如何保证它的安全和稳定,一直让我深感困惑。
但我的运气也是极好,就在我们聊完这个话题的第三天,字节的 Coze 宣布开源了!这无疑提供了一个绝佳的机会,去学习顶尖团队是如何设计这套系统的。
今天,就让我们一起深入 Coze 的源码,看看它是如何设计远程代码执行的安全架构的。
一、核心实现:两种模式的权衡
深入源码后,我发现 Coze 并没有采用"一刀切"的方案,而是设计了两套截然不同的执行引擎。
问题来了:为什么要设计两套方案呢?
这其实体现了工程设计中的一个重要原则——场景分离。在生产环境中,安全是第一要务;而在开发测试环境中,便捷性更为重要。
1. 生产环境的安全方案:Sandbox Runner
这是 Coze 用于生产环境的 Runner 实现,其核心是通过构建一个多层嵌套的沙箱,来实现安全隔离。
其执行链条如下: Go 主进程 -> Python 调度脚本 -> Deno 运行时 -> Pyodide (Wasm)
这种分层设计,确保了每一层都有独立的、明确的安全职责,从而构建起坚固的防御体系。
第一层:Go 调度层
Go 在这个架构中负责最高层的调度。它不直接执行用户的代码,而是通过 os/exec 启动一个独立的 Python 子进程,并利用 os.Pipe 创建的管道进行高效、安全的进程间通信,用于传递代码和参数。
// file: coze/coze-studio/backend/infra/impl/coderunner/sandbox/runner.go
func (runner *runner) Run(ctx context.Context, request *coderunner.RunRequest) (*coderunner.RunResponse, error) {
if request.Language == coderunner.JavaScript {
return nil, fmt.Errorf("js not supported yet") // JS被明确禁用
}
// 1. 将所有请求信息序列化为 JSON
b, err := json.Marshal(req{
Config: runner.config,
Code: request.Code,
Params: request.Params,
})
if err != nil {
return nil, err
}
// 2. 创建用于 "Go -> Python" 和 "Python -> Go" 的双向管道
pr, pw, err := os.Pipe() // pw (Go写) -> pr (Python读)
if err != nil {
return nil, err
}
r, w, err := os.Pipe() // w (Python写) -> r (Go读)
if err != nil {
return nil, err
}
// 3. 将数据写入管道并立即关闭写端
if _, err = pw.Write(b); err != nil {
return nil, err
}
if err = pw.Close(); err != nil {
return nil, err
}
// 4. 启动 Python 脚本子进程
cmd := exec.Command(runner.pyPath, runner.scriptPath)
// 5. 【关键】通过 `ExtraFiles` 将管道的文件描述符传递给子进程
cmd.ExtraFiles = []*os.File{w, pr}
if err = cmd.Start(); err != nil {
return nil, err
}
if err = w.Close(); err != nil { // 关闭Go端的写管道
return nil, err
}
// 6. 从输出管道中解码 Python 脚本返回的 JSON 结果
result := &resp{}
d := json.NewDecoder(r)
d.UseNumber() // 确保数字精度
if err = d.Decode(result); err != nil {
return nil, err
}
if err = cmd.Wait(); err != nil { // 等待子进程结束,回收资源
return nil, err
}
// 7. 检查执行状态
if result.Status != "success" {
return nil, fmt.Errorf("exec failed, stdout=%s, stderr=%s, sandbox_err=%s",
result.Stdout, result.Stderr, result.SandboxError)
}
return &coderunner.RunResponse{Result: result.Result}, nil
}
第二、三、四层:Python 调度与 Deno 执行
这个 Python 脚本是整个沙箱架构的核心。它接收到 Go 传来的数据后,并不会直接执行代码,而是继续将任务委托给 Deno 运行时。
它通过组装和执行一条 deno run 命令,并利用 Deno 强大的权限控制能力,对文件、网络、环境等进行精细化管理。
# file: coze/coze-studio/backend/infra/impl/coderunner/script/sandbox.py
class Sandbox:
def __init__(self, *, allow_env=False, allow_read=False, allow_write=False,
allow_net=False, allow_run=False, allow_ffi=False,
node_modules_dir="auto", **kwargs) -> None:
# 1. 根据 Go 传来的配置,生成 Deno 的权限参数列表
self.permissions = []
perm_defs = [
("--allow-env", allow_env, None),
("--allow-read", allow_read, ["node_modules"]),
("--allow-write", allow_write, ["node_modules"]),
("--allow-net", allow_net, None),
("--allow-run", allow_run, None),
("--allow-ffi", allow_ffi, None),
]
for flag, value, defaults in perm_defs:
perm = self._build_permission_flag(flag, value=value)
if perm is None and defaults is not None:
perm = f"{flag}={','.join(defaults)}"
if perm:
self.permissions.append(perm)
self.permissions.append(f"--node-modules-dir={node_modules_dir}")
def execute(self, code, *, timeout_seconds=None, memory_limit_mb=100, **kwargs) -> Output:
# 2. 组装 deno run 命令
cmd = ["deno", "run"]
cmd.extend(self.permissions)
# 3. 设置 V8 引擎参数(内存限制等)
v8_flags = ["--experimental-wasm-stack-switching"]
if memory_limit_mb is not None and memory_limit_mb > 0:
v8_flags.append(f"--max-old-space-size={memory_limit_mb}")
cmd.append(f"--v8-flags={','.join(v8_flags)}")
# 4. 运行一个基于 Pyodide (Python in Wasm) 的 JSR 包
cmd.append("jsr:@langchain/pyodide-sandbox@0.0.4")
cmd.extend(["--code", code])
# 5. 通过子进程运行 Deno,并捕获输出
try:
process = subprocess.run(cmd, capture_output=True, text=False,
timeout=timeout_seconds, check=False)
stdout = process.stdout.decode("utf-8", errors="replace")
if stdout:
full_result = json.loads(stdout)
result = full_result.get("result", None)
status = "success" if full_result.get("success", False) else "error"
else:
stderr = process.stderr.decode("utf-8", errors="replace")
status = "error"
except subprocess.TimeoutExpired:
status = "error"
stderr = f"Execution timed out after {timeout_seconds} seconds"
return Output(status=status, result=result, ...)
if __name__ == "__main__":
# 从 Go 进程传递来的文件描述符中读取输入
w = os.fdopen(3, "wb") # 写给 Go
r = os.fdopen(4, "rb") # 从 Go 读
try:
req = json.load(r)
user_code, params, config = req["code"], req["params"], req["config"] or {}
# 创建沙箱实例并执行
sandbox = Sandbox(**config)
# 包装用户代码为完整的 Python 脚本
if params is not None:
code = prefix + f'args={json.dumps(params)}\n' + user_code + suffix
else:
code = prefix + user_code + suffix
resp = sandbox.execute(code, **config)
# 将最终结果写回给 Go 进程
result = json.dumps(dataclasses.asdict(resp), ensure_ascii=False)
w.write(str.encode(result))
w.flush()
w.close()
except Exception as e:
# 异常情况下也要回传错误信息
w.write(str.encode(json.dumps({"sandbox_error": str(e)})))
w.flush()
w.close()
这意味着用户的 Python 代码,最终是在一个由 Deno 严格限制权限、并运行在 WebAssembly 虚拟机内的 Pyodide 解释器中执行的。这四层嵌套构建了极为坚固的安全壁垒。
2. 开发环境的便捷方案:Direct Runner
除了相对复杂的沙箱执行器,Coze 还提供了一个简单的 direct/runner.go 实现。它没有层层嵌套,而是直接通过 exec.Command 调用系统安装的 Python 解释器来执行代码。
源码中 // ignore_security_alert RCE 的注释清晰地表明,这种方式存在远程代码执行风险,主要用于本地开发或测试等受信任的场景。
3. 统一的安全前置校验
无论使用哪种 Runner,代码在被执行前,都会经过上层业务逻辑的严格校验,特别是针对 Python 的模块导入。
就像机场安检一样,危险物品根本不允许带上飞机。
// file: coze/coze-studio/backend/domain/workflow/internal/nodes/code/code.go
// 完整的模块黑名单
var pythonBuiltinBlacklist = map[string]struct{}{
"curses": {}, "dbm": {}, "multiprocessing": {}, "threading": {},
"socket": {}, "pty": {}, "tty": {}, "fcntl": {}, "grp": {},
"pwd": {}, "resource": {}, "syslog": {}, "termios": {}, // ... 更多模块
}
// 第三方库白名单(非常有限)
var pythonThirdPartyWhitelist = map[string]struct{}{
"requests_async": {},
"numpy": {},
}
func validatePythonImports(code string) error {
imports := parsePythonImports(code) // 解析代码中的所有 import 语句
importErrors := make([]string, 0)
var blacklistedModules []string
var nonWhitelistedModules []string
for _, imp := range imports {
if _, ok := pythonBuiltinModules[imp]; ok {
// 检查是否在内置模块的黑名单中
if _, blacklisted := pythonBuiltinBlacklist[imp]; blacklisted {
blacklistedModules = append(blacklistedModules, imp)
}
} else {
// 检查是否在第三方库的白名单中
if _, whitelisted := pythonThirdPartyWhitelist[imp]; !whitelisted {
nonWhitelistedModules = append(nonWhitelistedModules, imp)
}
}
}
// 生成具体的错误消息
if len(blacklistedModules) > 0 {
moduleNames := fmt.Sprintf("'%s'", strings.Join(blacklistedModules, "', '"))
importErrors = append(importErrors, fmt.Sprintf(
"ModuleNotFoundError: The module(s) %s are removed from the Python standard library for security reasons",
moduleNames))
}
if len(nonWhitelistedModules) > 0 {
moduleNames := fmt.Sprintf("'%s'", strings.Join(nonWhitelistedModules, "', '"))
importErrors = append(importErrors, fmt.Sprintf(
"ModuleNotFoundError: No module named %s", moduleNames))
}
if len(importErrors) > 0 {
return errors.New(strings.Join(importErrors, ","))
}
return nil
}
这一机制与 Coze 官方文档 的说明一致,是独立于 Runner 实现的、前置的第一道安全防线。
二、执行流程
下图清晰地展示了 Sandbox Runner 的完整执行流程:
sequenceDiagram
participant User as "用户"
participant GoBackend as "Coze Go 后端"
participant PyScheduler as "sandbox.py 调度进程"
participant DenoWasm as "Deno & Pyodide (Wasm)"
User->>GoBackend: 触发包含代码块的工作流
GoBackend->>GoBackend: 1. 前置安全校验 (模块导入等)
GoBackend->>PyScheduler: 2. exec() 启动子进程<br/>并通过 os.Pipe 传递代码和配置
PyScheduler->>DenoWasm: 3. subprocess() 启动 Deno 进程<br/>并设置权限 flags
DenoWasm->>DenoWasm: 4. 在 Wasm 虚拟机中<br/>执行 Python 代码
DenoWasm-->>PyScheduler: 5. 返回执行结果
PyScheduler-->>GoBackend: 6. 将结果通过管道返回
GoBackend->>GoBackend: 7. 格式化结果
GoBackend-->>User: 8. 返回最终输出
三、回到最初的问题
现在,我们可以回答文章开头提出的那个问题了:面对低代码平台中那个棘手的"代码块"功能,我们该如何保证其安全与稳定?
通过对 Coze 源码的这次探索,我找到了一个堪称教科书级别的答案。其核心设计思想如下:
- 极致分层的安全沙箱:用
Go -> Python -> Deno -> Wasm的层层嵌套,构建起纵深防御体系,将风险隔离在最小的单元内。 - 兼顾安全与效率的场景化设计:为生产环境和开发环境提供不同的执行器(
SandboxvsDirect),在不同场景下做出了最优的平衡。 - 从源头拦截风险的前置校验:在代码真正执行前,通过严格的模块黑白名单机制,提前拆除"炸弹",而不是等它爆炸。
这套集隔离、分离、前置于一体的完整方案非常值得学习!
Make Open Source Great Again!
👨💻 关于十三 Tech
资深服务端研发工程师,AI 编程实践者。
专注分享真实的技术实践经验,相信 AI 是程序员的最佳搭档。
希望能和大家一起写出更优雅的代码!
📧 联系方式:569893882@qq.com
🌟 GitHub:@TriTechAI
💬 VX:TriTechAI(备注:十三 Tech)