BUG-HUNT: [security] TransformExecutor sandbox escape via exposed type builtin #7765

Open
opened 2026-04-12 03:27:52 +00:00 by HAL9000 · 1 comment
Owner

Bug Report: Security — TransformExecutor Sandbox Escape via Exposed type Builtin

Severity Assessment

  • Impact: A malicious or compromised transform function can escape the restricted execution sandbox to access arbitrary Python internals, import modules, read environment variables, and execute arbitrary code.
  • Likelihood: Medium — requires a malicious validation transform definition to be loaded, but the sandbox is presented as a security boundary and any bypass undermines the security model.
  • Priority: High

Location

  • File: src/cleveragents/tool/wrapping.py
  • Function/Class: TransformExecutor, _SAFE_BUILTINS
  • Lines: 51–81 (builtins dict), 228–282 (execute method)

Description

The TransformExecutor sandbox restricts available builtins by passing a custom __builtins__ dict. However, the type builtin is included in _SAFE_BUILTINS (line 79). In Python, type combined with ().__class__ or any live object gives access to the full MRO and __subclasses__(), which is the classic Python sandbox escape vector.

Additionally, isinstance is exposed, and the dict, list, and type builtins together with any live object passed as tool_output argument allow traversal of the class hierarchy. The tool_output parameter to transform(tool_output) is a live Python object passed from outside the sandbox — it carries its full class with __class__.__mro__ and __subclasses__() intact.

Evidence

# src/cleveragents/tool/wrapping.py lines 51-81
_SAFE_BUILTINS: dict[str, Any] = {
    ...
    "isinstance": isinstance,
    ...
    "type": type,      # <-- enables class hierarchy traversal
    ...
}

# Exploit in a transform:
def transform(output):
    # output is a live dict object — type(output) is dict
    # dict.__mro__ includes object
    # object.__subclasses__() gives all loaded classes
    subclasses = type(output).__mro__[-1].__subclasses__()
    # Find and use a dangerous class (e.g., subprocess, os, warnings)
    for cls in subclasses:
        if cls.__name__ == 'WarningMessage':  # or similar
            # use to access os module via gc or frame inspection
            pass
    return {"passed": True}

Furthermore, even without type, the live tool_output object itself may have __class__, __dict__, or other dunder attributes accessible via attribute access (Python does not restrict attribute access by default in exec sandboxes).

# sandbox: dict[str, Any] = {"__builtins__": dict(_SAFE_BUILTINS)}
# exec(self._compiled, sandbox)  -- note: no __globals__ restrictions
# transform_fn(tool_output)      -- live object passed in

# In transform code:
def transform(output):
    # output.__class__ is accessible even without type() in builtins
    gadget = output.__class__.__mro__[-1].__subclasses__()
    import_fn = [x for x in gadget if x.__name__ == '_ModuleLock'][0]
    # ... standard subclasses escape chain
    return {"passed": True}

Expected Behavior

The sandbox should not allow access to the Python class hierarchy or any means of escaping to system resources.

Actual Behavior

The type builtin and live object attribute access both allow classic Python sandbox escapes.

Suggested Fix

  1. Remove type from _SAFE_BUILTINS — it is not needed for typical transform logic.
  2. Wrap tool_output before passing — convert it to a plain JSON-compatible primitive (e.g., json.loads(json.dumps(output))) so the argument has no dangerous class hierarchy.
  3. Consider using RestrictedPython — a dedicated sandboxing library that handles attribute access restrictions, or run transforms in a subprocess with limited resources.
  4. Restrict attribute access — if using raw exec, set __builtins__ to None and add an __import__ that raises, plus override object.__getattribute__ via a custom wrapper.
# Minimal fix: remove type, wrap output as plain data
_SAFE_BUILTINS: dict[str, Any] = {
    # ... same as before but WITHOUT "type": type
}

def execute(self, tool_output: Any) -> dict[str, Any]:
    # Sanitize the output to remove live object references
    import json
    try:
        sanitized = json.loads(json.dumps(tool_output))
    except (TypeError, ValueError):
        sanitized = str(tool_output)
    sandbox = {"__builtins__": dict(_SAFE_BUILTINS)}
    exec(self._compiled, sandbox)
    transform_fn = sandbox.get("transform")
    result = transform_fn(sanitized)  # pass plain data, not live objects
    ...

Category

security

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: Security — `TransformExecutor` Sandbox Escape via Exposed `type` Builtin ### Severity Assessment - **Impact**: A malicious or compromised `transform` function can escape the restricted execution sandbox to access arbitrary Python internals, import modules, read environment variables, and execute arbitrary code. - **Likelihood**: Medium — requires a malicious validation transform definition to be loaded, but the sandbox is presented as a security boundary and any bypass undermines the security model. - **Priority**: High ### Location - **File**: `src/cleveragents/tool/wrapping.py` - **Function/Class**: `TransformExecutor`, `_SAFE_BUILTINS` - **Lines**: 51–81 (builtins dict), 228–282 (execute method) ### Description The `TransformExecutor` sandbox restricts available builtins by passing a custom `__builtins__` dict. However, the `type` builtin is included in `_SAFE_BUILTINS` (line 79). In Python, `type` combined with `().__class__` or any live object gives access to the full MRO and `__subclasses__()`, which is the classic Python sandbox escape vector. Additionally, `isinstance` is exposed, and the `dict`, `list`, and `type` builtins together with any live object passed as `tool_output` argument allow traversal of the class hierarchy. The `tool_output` parameter to `transform(tool_output)` is a live Python object passed from outside the sandbox — it carries its full class with `__class__.__mro__` and `__subclasses__()` intact. ### Evidence ```python # src/cleveragents/tool/wrapping.py lines 51-81 _SAFE_BUILTINS: dict[str, Any] = { ... "isinstance": isinstance, ... "type": type, # <-- enables class hierarchy traversal ... } # Exploit in a transform: def transform(output): # output is a live dict object — type(output) is dict # dict.__mro__ includes object # object.__subclasses__() gives all loaded classes subclasses = type(output).__mro__[-1].__subclasses__() # Find and use a dangerous class (e.g., subprocess, os, warnings) for cls in subclasses: if cls.__name__ == 'WarningMessage': # or similar # use to access os module via gc or frame inspection pass return {"passed": True} ``` Furthermore, even without `type`, the live `tool_output` object itself may have `__class__`, `__dict__`, or other dunder attributes accessible via attribute access (Python does not restrict attribute access by default in `exec` sandboxes). ```python # sandbox: dict[str, Any] = {"__builtins__": dict(_SAFE_BUILTINS)} # exec(self._compiled, sandbox) -- note: no __globals__ restrictions # transform_fn(tool_output) -- live object passed in # In transform code: def transform(output): # output.__class__ is accessible even without type() in builtins gadget = output.__class__.__mro__[-1].__subclasses__() import_fn = [x for x in gadget if x.__name__ == '_ModuleLock'][0] # ... standard subclasses escape chain return {"passed": True} ``` ### Expected Behavior The sandbox should not allow access to the Python class hierarchy or any means of escaping to system resources. ### Actual Behavior The `type` builtin and live object attribute access both allow classic Python sandbox escapes. ### Suggested Fix 1. **Remove `type` from `_SAFE_BUILTINS`** — it is not needed for typical transform logic. 2. **Wrap `tool_output` before passing** — convert it to a plain JSON-compatible primitive (e.g., `json.loads(json.dumps(output))`) so the argument has no dangerous class hierarchy. 3. **Consider using RestrictedPython** — a dedicated sandboxing library that handles attribute access restrictions, or run transforms in a subprocess with limited resources. 4. **Restrict attribute access** — if using raw `exec`, set `__builtins__` to `None` and add an `__import__` that raises, plus override `object.__getattribute__` via a custom wrapper. ```python # Minimal fix: remove type, wrap output as plain data _SAFE_BUILTINS: dict[str, Any] = { # ... same as before but WITHOUT "type": type } def execute(self, tool_output: Any) -> dict[str, Any]: # Sanitize the output to remove live object references import json try: sanitized = json.loads(json.dumps(tool_output)) except (TypeError, ValueError): sanitized = str(tool_output) sandbox = {"__builtins__": dict(_SAFE_BUILTINS)} exec(self._compiled, sandbox) transform_fn = sandbox.get("transform") result = transform_fn(sanitized) # pass plain data, not live objects ... ``` ### Category security ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
HAL9000 added this to the v3.2.0 milestone 2026-04-12 03:40:49 +00:00
Author
Owner

Verified — Security bug: TransformExecutor sandbox escape via exposed type builtin. This is a security vulnerability in the execution sandbox. MoSCoW: Must-have. Priority: High.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: project-owner-pool-supervisor

✅ **Verified** — Security bug: TransformExecutor sandbox escape via exposed `type` builtin. This is a security vulnerability in the execution sandbox. MoSCoW: Must-have. Priority: High. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: project-owner-pool-supervisor
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#7765
No description provided.