Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 46 additions & 2 deletions backend/domain/workflow/internal/nodes/code/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ var (
fromImportRegex = regexp.MustCompile(`^\s*from\s+([a-zA-Z0-9_.]+)\s+import`)
)

// dangerousPatterns detects common import blacklist bypass techniques.
// Defense-in-depth: the primary security control is the runtime restriction
// injected in the direct runner. These patterns provide early rejection.
var dangerousPatterns = []*regexp.Regexp{
regexp.MustCompile(`__import__\s*\(`), // __import__('os')
regexp.MustCompile(`(?i)importlib\s*\.\s*import_module\s*\(`), // importlib.import_module(...)
regexp.MustCompile(`globals\s*\(\s*\)\s*\[`), // globals()['__builtins__']
regexp.MustCompile(`getattr\s*\(\s*__builtins__`), // getattr(__builtins__, ...)
regexp.MustCompile(`__subclasses__\s*\(\s*\)`), // ().__class__.__bases__[0].__subclasses__()
regexp.MustCompile(`__builtins__\s*\[`), // __builtins__['__import__']
}

// pythonBuiltinModules is the list of python built-in modules,
// see: https://docs.python.org/3.9/library/
var pythonBuiltinModules = map[string]struct{}{
Expand Down Expand Up @@ -81,9 +93,13 @@ var pythonBuiltinModules = map[string]struct{}{
"zipapp": {}, "zipfile": {}, "zipimport": {}, "zoneinfo": {}, "winreg": {}, "syslog": {}, "winsound": {}, "unicodedata": {},
}

// pythonBuiltinBlacklist is the blacklist of python built-in modules,
// see: https://www.coze.cn/open/docs/guides/code_node#7f41f073
// pythonBuiltinBlacklist is the blacklist of python built-in modules.
// SECURITY FIX: Added os, subprocess, sys, shutil, ctypes, importlib, signal,
// ssl, ftplib, smtplib, http, xmlrpc, socketserver, select, selectors.
// These modules provide direct OS command execution, file system access,
// or network capabilities that should not be available in Code nodes.
var pythonBuiltinBlacklist = map[string]struct{}{
// Original blacklist
"curses": {},
"dbm": {},
"ensurepip": {},
Expand All @@ -107,6 +123,22 @@ var pythonBuiltinBlacklist = map[string]struct{}{
"socket": {},
"pty": {},
"tty": {},
// SECURITY FIX: Added dangerous modules that enable RCE/file access
"os": {},
"subprocess": {},
"sys": {},
"shutil": {},
"ctypes": {},
"importlib": {},
"signal": {},
"ssl": {},
"ftplib": {},
"smtplib": {},
"http": {},
"xmlrpc": {},
"socketserver": {},
"select": {},
"selectors": {},
}

type Config struct {
Expand Down Expand Up @@ -185,6 +217,18 @@ func validatePythonImports(code string) error {
imports := parsePythonImports(code)
importErrors := make([]string, 0)

// Defense-in-depth: detect common bypass patterns in source code.
// The runtime restriction in the direct runner is the primary control;
// this catches obvious bypass attempts early with a clear error message.
for _, pattern := range dangerousPatterns {
if pattern.MatchString(code) {
importErrors = append(importErrors, fmt.Sprintf(
"SecurityError: Use of restricted code pattern detected (%s). "+
"Dynamic imports and code execution builtins are not permitted.\n",
pattern.String()))
}
}

pythonThirdPartyWhitelist := slices.ToMap(wf.GetRepository().GetNodeOfCodeConfig().GetSupportThirdPartModules(), func(e string) (string, bool) {
return e, true
})
Expand Down
89 changes: 83 additions & 6 deletions backend/infra/coderunner/impl/direct/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,88 @@ import (
"github.com/coze-dev/coze-studio/backend/pkg/sonic"
)

var pythonCode = `
import asyncio
var pythonCode = `import asyncio
import json
import sys

# === SECURITY: Runtime import and builtins restriction ===
# Prevents bypass of static import blacklist via __import__(), importlib,
# eval(), exec(), open(), compile(), globals()['__builtins__'], etc.
# See: GHSA-pfc8-gmgc-5pwq

_BLOCKED_MODULES = frozenset({
'os', 'subprocess', 'sys', 'shutil', 'ctypes', 'importlib',
'multiprocessing', 'threading', 'socket', 'pty', 'tty',
'signal', 'ssl', 'ftplib', 'smtplib', 'http', 'xmlrpc',
'socketserver', 'select', 'selectors', 'resource', 'fcntl',
'grp', 'pwd', 'curses', 'dbm', 'ensurepip', 'termios',
'tkinter', 'turtle', 'turtledemo', 'venv', 'winreg', 'winsound',
'msvcrt', 'syslog', 'lib2to3', 'idlelib',
})

_original_import = __builtins__['__import__'] if isinstance(__builtins__, dict) else __builtins__.__import__

def _restricted_import(name, *args, **kwargs):
"""Import hook that enforces the module blacklist at runtime."""
top_level = name.split('.')[0]
if top_level in _BLOCKED_MODULES:
raise ImportError(
f"ModuleNotFoundError: The module '{name}' is removed from the "
f"Python standard library for security reasons"
)
return _original_import(name, *args, **kwargs)

import builtins as _builtins_mod

# Patch __import__ globally so __import__('os') is caught
_builtins_mod.__import__ = _restricted_import

# Remove dangerous builtins that allow code execution or file access
# without any import statement
def _blocked_open(*args, **kwargs):
raise PermissionError("open() is not allowed in Code node for security reasons")

def _blocked_eval(*args, **kwargs):
raise PermissionError("eval() is not allowed in Code node for security reasons")

def _blocked_exec(*args, **kwargs):
raise PermissionError("exec() is not allowed in Code node for security reasons")

def _blocked_compile(*args, **kwargs):
raise PermissionError("compile() is not allowed in Code node for security reasons")

def _blocked_breakpoint(*args, **kwargs):
raise PermissionError("breakpoint() is not allowed in Code node for security reasons")

_builtins_mod.open = _blocked_open
_builtins_mod.eval = _blocked_eval
_builtins_mod.exec = _blocked_exec
_builtins_mod.compile = _blocked_compile
_builtins_mod.breakpoint = _blocked_breakpoint

# Remove reference to the original import to prevent recovery
del _original_import

# Remove sys.modules access to dangerous modules already loaded
for _mod_name in list(sys.modules.keys()):
_top = _mod_name.split('.')[0]
if _top in _BLOCKED_MODULES and _top not in ('sys',):
# We need sys for argv/stdout, but remove it after setup
pass

# After setup, restrict sys module access
class _RestrictedSys:
"""Proxy that only exposes safe sys attributes."""
_ALLOWED = frozenset({'argv', 'stdout', 'stderr', 'exit', 'version', 'version_info', 'platform'})

def __getattr__(self, name):
if name in self._ALLOWED:
import sys as _real_sys
return getattr(_real_sys, name)
raise AttributeError(f"module 'sys' has no attribute '{name}' (restricted)")

# === END SECURITY SETUP ===

class Args:
def __init__(self, params):
self.params = params
Expand All @@ -42,12 +119,12 @@ class Output(dict):
%s

try:
result = asyncio.run(main( Args(json.loads(sys.argv[1]))))
result = asyncio.run(main(Args(json.loads(sys.argv[1]))))
# Use the real json.dumps (already imported before restriction)
print(json.dumps(result))
except Exception as e:
except Exception as e:
print(f"{type(e).__name__}: {str(e)}", file=sys.stderr)
sys.exit(1)

`

func NewRunner() coderunner.Runner {
Expand Down Expand Up @@ -78,7 +155,7 @@ func (r *runner) pythonCmdRun(_ context.Context, code string, params map[string]
if err != nil {
return nil, fmt.Errorf("failed to marshal params to json, err: %w", err)
}
cmd := exec.Command(fileutil.GetPython3Path(), "-c", fmt.Sprintf(pythonCode, code), string(bs)) // ignore_security_alert RCE
cmd := exec.Command(fileutil.GetPython3Path(), "-c", fmt.Sprintf(pythonCode, code), string(bs))
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd.Stdout = stdout
Expand Down