diff --git a/backend/domain/workflow/internal/nodes/code/code.go b/backend/domain/workflow/internal/nodes/code/code.go index f2deadd034..e7232ee678 100644 --- a/backend/domain/workflow/internal/nodes/code/code.go +++ b/backend/domain/workflow/internal/nodes/code/code.go @@ -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{}{ @@ -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": {}, @@ -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 { @@ -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 }) diff --git a/backend/infra/coderunner/impl/direct/runner.go b/backend/infra/coderunner/impl/direct/runner.go index a85c387506..75c7574fda 100644 --- a/backend/infra/coderunner/impl/direct/runner.go +++ b/backend/infra/coderunner/impl/direct/runner.go @@ -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 @@ -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 { @@ -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