Skip to content

Script sandbox is fully bypassed via types.CodeType/FunctionType bytecode reconstruction #8927

Open

Description

@carfeii

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

  1. self-assigned this
    on Oct 16, 2026
  2. sardortaylakov commented on Oct 20, 2026

    @sardortaylakov
    Contributor

    #8937 addressed this bypass by blocking types imports and access to CodeType, FunctionType, and LambdaType. I opened an small PR with targeted regression tests for aliases, re-exported constructors, and a working bytecode payload that calls eval("6 * 7") through importlib when validation is skipped. @AronPerez please see if it adds any value to the codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions