Repository navigation
Script sandbox is fully bypassed via types.CodeType/FunctionType bytecode reconstruction #8927
Description
reported on 9 sep 2026 via https://github.com/Skyvern-AI/skyvern/security/advisories/GHSA-563g-mp6w-57r2:
Summary
Skyvern's "Scripts" feature runs uploaded Python files as an importlib-loaded module with genuine builtins attached and, by design, no runtime sandbox: the static AST check is_safe_script_code (skyvern/forge/sdk/workflow/code_block_safety.py) is the only gate. That check blocks named dangerous builtins, attributes, and imports (subprocess, socket, eval, exec, and, until GHSA-xp6f-4rhc-8xfw, open), but it operates entirely on the AST: it inspects ast.Call, ast.Attribute, ast.Name, and ast.Import nodes. types.CodeType and types.FunctionType let a script build a fully independent function object from raw bytecode bytes, then call it. Nothing inside that reconstructed function is visible to the AST walk, since the function body is a bytes literal, not Python source the checker parses. The reconstructed function runs with the same genuine builtins as the rest of the module, so it can call open, os.system, socket.connect, or anything else, regardless of what the denylist blocks.
Details
is_safe_script_code walks the AST once and rejects specific node shapes (blocked import roots, blocked attribute names, blocked bare names, private/dunder access). It has no way to inspect what a bytes object passed to types.CodeType will do once turned into an executable code object; the check is fundamentally a syntax-level filter, and types.CodeType/types.FunctionType are semantics that operate below the syntax level.
skyvern/forge/sdk/workflow/service.py, _load_user_script_module (the runtime that later loads and runs an accepted script):
loaded_script_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script_module)This is exactly the "real importlib-loaded module with the genuine __builtins__ attached" the checker's own docstring describes.
Proof of Concept
(available upon request)
Impact
This is a complete bypass of the Script sandbox, not limited to file access. The reconstructed function's bytecode can contain any sequence of instructions the interpreter supports: IMPORT_NAME for any module (including ones in BLOCKED_SCRIPT_IMPORTS, since the checker only inspects ast.Import/ast.ImportFrom nodes in the submitted source, not names the reconstructed bytecode imports at runtime), then a call to subprocess.run, socket.connect, or anything else the process's genuine builtins expose. Every existing denylist entry (subprocess, socket, ctypes, pickle, the frame/dunder escape list) is irrelevant once a script can build and run bytecode the checker never parses as Python source.
Affected: github.com/Skyvern-AI/skyvern, confirmed on commit 1c978f3380f723681382d832504922e4408c8a6e (current main), latest release v1.0.52.
Fix
Added types to BLOCKED_SCRIPT_IMPORTS (the primary block: a script can no longer reach types.CodeType/types.FunctionType via import types at all) and CodeType/FunctionType to BLOCKED_ATTRS (defense in depth: the existing ImportFrom alias check already rejects from types import CodeType once these names are in blocked_attrs, and this also covers any other module that might re-export the same classes).
New regression tests in tests/unit/test_code_block_sandbox.py, TestScriptSandboxRejectsBytecodeConstruction: one confirms is_safe_script_code now rejects the reconstruction payload, the second runs the pre-fix payload through the actual importlib load path to document that it was a genuine, working exploit. Full tests/unit/test_code_block_sandbox.py suite (286 tests) passes.
This closes the specific gadget found here. A denylist-based AST check for an arbitrary-execution surface is inherently incomplete against novel gadgets; flagging that as a structural note for the maintainer rather than a claim that no further bypass exists.
Activity
sardortaylakov commentedon Oct 20, 2026
#8937 addressed this bypass by blocking
typesimports and access toCodeType,FunctionType, andLambdaType. I opened an small PR with targeted regression tests for aliases, re-exported constructors, and a working bytecode payload that callseval("6 * 7")throughimportlibwhen validation is skipped. @AronPerez please see if it adds any value to the codebase.