Skip to content

[security] fix(tools): harden Python tool sandbox#94

Merged
mbakgun merged 1 commit into
heymrun:mainfrom
Hinotoi-agent:fix/python-tool-sandbox-escape
May 10, 2026
Merged

[security] fix(tools): harden Python tool sandbox#94
mbakgun merged 1 commit into
heymrun:mainfrom
Hinotoi-agent:fix/python-tool-sandbox-escape

Conversation

@Hinotoi-agent
Copy link
Copy Markdown
Contributor

Summary

This PR hardens custom Python tool execution for AI Agent workflows. The previous runner relied on a blacklist of dangerous builtins and modules, but tool code could still use Python object-graph introspection to recover the real __import__, import blocked modules such as os, and execute host commands or read inherited backend environment variables.

The patch adds a pre-execution safety validation step for restricted introspection primitives, removes high-risk builtins that expose the Python object graph, and launches the tool subprocess with a scrubbed environment and isolated temporary working directory.

Security issues covered

Issue Severity Boundary
Python tool sandbox escape through object-graph introspection Critical User-authored workflow tool code to backend host process

Before this PR

  • Custom Python tool code was executed with exec(code, namespace) in a child Python process.
  • The child runner removed direct access to builtins such as open, eval, exec, getattr, and __import__.
  • The blacklist could be bypassed with introspection primitives such as ().__class__.__base__.__subclasses__() and warnings.catch_warnings.
  • The child process inherited the backend process environment and current working directory.

After this PR

  • Tool code is rejected before execution if it contains restricted Python introspection primitives such as dunder access, catch_warnings, frame/global escape handles, or private attribute access.
  • object and type are no longer available to tool code as safe builtins.
  • Python tool subprocesses run with a minimal environment instead of inheriting backend secrets.
  • Python tool subprocesses run from a temporary execution directory rather than the backend working tree.
  • Regression coverage confirms normal tool code still executes and the object-graph import bypass is rejected.

Why this matters

Heym exposes custom Python tools as part of AI Agent workflow execution. A user who can create or edit a workflow can provide Python tool code. Because the runner attempted to sandbox that code, blocked modules such as os and subprocess should not be reachable from the tool body.

The object-graph bypass crossed that boundary. In a hosted or multi-user deployment, a malicious workflow tool could execute commands as the backend service user, read inherited secrets such as database or encryption settings, and use those secrets to affect other tenants or backend resources.

Attack flow

  1. Attacker creates or edits a workflow containing an AI Agent node with a custom Python tool.
  2. The tool body uses Python object-graph introspection to locate warnings.catch_warnings.
  3. From that object, the tool recovers the real builtins dictionary and unrestricted __import__.
  4. The tool imports blocked modules such as os.
  5. The tool reads backend environment variables or runs host commands.
  6. The workflow execution returns the result through the normal tool output path.

Affected code

  • backend/app/services/python_tool_executor.py
    • _RUNNER_SCRIPT executed user-provided tool code with exec(code, namespace).
    • execute_tool() launched the runner without an explicit environment or isolated working directory.
  • Workflow execution reaches this runner through AI Agent tool execution paths.

Root cause

  • The sandbox relied on blacklist-based builtin/module filtering inside Python.
  • Python object-graph introspection still exposed runtime objects that reference the original builtins.
  • The child process inherited ambient backend process state, so a sandbox escape could immediately read secrets from the environment.

CVSS assessment

  • Issue: Python tool sandbox escape to backend command execution / secret exposure
  • CVSS v3.1: 9.9 Critical
  • Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
  • Rationale: a low-privileged authenticated workflow author can cross from intended tool-code restrictions into backend host execution and inherited backend secret access. Scope is changed because backend process secrets and host resources can affect other users/tenants in shared deployments.

Safe reproduction steps

Run this against the vulnerable code from the backend package context:

HEYM_PYTHON_TOOL_SECRET=topsecret python3 - <<'PY'
import sys
sys.path.insert(0, '/tmp/heym-audit/backend')
from app.services.python_tool_executor import execute_tool

code = '''
def leak():
    cw = [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]
    imp = cw()._module.__builtins__['__import__']
    os = imp('os')
    return {
        'env': os.environ.get('HEYM_PYTHON_TOOL_SECRET'),
        'id': os.popen('id').read().strip(),
    }
'''
print(execute_tool(code, 'leak', {}, 5))
PY

Expected vulnerable behavior

On vulnerable code, the tool returns the inherited secret and command output, for example:

{'env': 'topsecret', 'id': 'uid=...'}

With this PR, the same payload is rejected before execution with:

ValueError Tool error: Tool code contains a restricted Python introspection primitive

Changes in this PR

  • Adds restricted-fragment and AST validation before executing custom Python tool code.
  • Rejects dunder/introspection primitives and private attribute access in tool code.
  • Removes object and type from the safe builtin set.
  • Launches the Python tool runner with a minimal explicit environment.
  • Runs tool subprocesses from a fresh temporary directory.
  • Adds focused regression tests for the sandbox escape and baseline safe tool execution.

Files changed

Category Files What changed
Sandbox hardening backend/app/services/python_tool_executor.py Pre-execution validation, tighter builtin filtering, scrubbed env, isolated cwd
Tests backend/tests/test_python_tool_executor.py Regression tests for benign execution and blocked introspection escape

Maintainer impact

  • Normal simple Python tools continue to run.
  • Tool code that depends on private/dunder Python introspection is now rejected.
  • The patch is intentionally conservative because private Python runtime access is not a safe boundary for untrusted workflow tool code.
  • This does not claim to make Python a perfect in-process sandbox; stronger process/container isolation remains the best long-term boundary for untrusted code.

Fix rationale

The immediate exploitable path was Python runtime introspection plus ambient process inheritance. Blocking private/dunder introspection removes the demonstrated escape primitive, while launching the child runner with a minimal environment reduces blast radius if another escape is found later.

Type of change

  • Security fix
  • Regression tests
  • Documentation update
  • Dependency change

Test plan

Executed locally:

cd /tmp/heym-audit/backend
uv run pytest tests/test_python_tool_executor.py -q

Result:

4 passed
cd /tmp/heym-audit/backend
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
  uv run pytest tests/test_workflow_execution_api.py tests/test_workflow_executor_branching.py tests/test_python_tool_executor.py -q

Result:

40 passed, 4 warnings
cd /tmp/heym-audit
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ./run_tests.sh

Result:

742 tests, 742 passed — All test suites passed.
cd /tmp/heym-audit/backend
uv run ruff check .
uv run ruff format --check .

Result:

All checks passed!
221 files already formatted

./check.sh could not complete locally because the environment does not have bun installed:

Running frontend checks...
./check.sh: line 10: bun: command not found

Disclosure notes

This PR is bounded to the Python tool runner sandbox escape and inherited-process-state exposure. It does not address unrelated workflow authorization or file-upload findings, which are covered separately.

@mbakgun
Copy link
Copy Markdown
Contributor

mbakgun commented May 10, 2026

Thanks ! 🙏

@mbakgun mbakgun merged commit 32b7e80 into heymrun:main May 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants