Inline-code sandbox exposes __import__, letting code bypass the Restricted Built-ins set entirely (e.g. import os) #107

Open
opened 2026-08-04 18:11:49 +00:00 by CoreRasurae · 0 comments
Member

Metadata

  • Commit Message: fix(agents): restrict inline-code sandbox to a json-only __import__
  • Branch: bugfix/m1-inline-sandbox-import-restriction (provisional — align the milestone number with the milestone assigned at triage)

Background and context

ToolAgent._execute_python_code (cleveractors.agents.tool) builds the inline-code sandbox's __builtins__ dict per §13.2.1's "Restricted Built-ins for Inline Code" table. That table is documented as exhaustive (§13.2.1: "the following built-in facilities MUST be available to it, and no other built-in facilities MAY be exposed") and does not list __import__. §13.2.3 additionally prohibits, without qualification, "performing arbitrary I/O against the filesystem, network, or system" and "dynamic import of modules other than those explicitly listed" for both sandboxes defined by the standard.

Despite this, _execute_python_code's safe_globals["__builtins__"] dict includes "__import__": __import__ — a direct reference to the real, unrestricted import machinery — with the accompanying comment "Allow imports for json." Because __import__ is the actual mechanism Python's import statement invokes, an inline-code body can write import os, import subprocess, import socket, etc. and reach exactly the facilities (os, io, pathlib, filesystem, network, process control) that §13.2.1's table and §13.2.3's prohibitions were written to keep out.

This predates ADR-2035 (issue #93) and was flagged during that PR's review (#103) as pre-existing and explicitly out of scope for that change (ADR-2035 D-1 fixes the built-in table as unchanged), but it is a live security gap: while __import__ remains, the inline-code sandbox is not a real security boundary — it is advisory at best. The new ADR-2035 read_file/write_file helpers' sandbox-root confinement is consequently only a convenience / defense-in-depth control today, not a hard limit, since any inline body can bypass it entirely via import os; os.open(...) or equivalent.

Current behavior

Inline code (a type: tool agent's code: body, or the python_exec built-in tool, which shares the same sandbox construction) can execute import os (or any other module) successfully, then use that module's full API — including arbitrary filesystem access, network access, and subprocess execution — with no restriction. Only the name json is meant to be reachable via import per the spec's intent, but nothing currently stops any other module name.

Expected behavior

Only json (the one module §13.2.1 names) is importable via an import statement inside inline code; importing any other module raises the same category of error inline code already gets for referencing any other undefined/prohibited name. Per §13.2.1, this MUST hold for the inline-code sandbox; §13.2.2 (the expression sandbox) is unaffected since it never exposed __import__ to begin with.

Acceptance criteria

  • import json (and the pre-bound json local name already relied on by the §4.5.2 canonical example) continues to work unchanged inside inline code.
  • import os (and any other non-json module) inside inline code raises an error — a category consistent with how _execute_python_code already reports prohibited-name access (see its NameErrorExecutionError translation), not a bare Python traceback.
  • The fix does not add any new name to the exposed __builtins__ table beyond what §13.2.1 already lists (json stays the only module-shaped facility).
  • python_exec (the exec_python-gated built-in tool, which shares _execute_python_code) is restricted identically — no separate bypass path.
  • The §13.2.2 expression sandbox (transform.fn, bridge predicates) is unaffected — it already has no __import__ and gains none.
  • No existing Behave/Robot scenario that relies on import json inside inline code regresses.

Supporting information

  • Origin: src/cleveractors/agents/tool.py, ToolAgent._execute_python_code, safe_globals["__builtins__"]["__import__"].
  • Spec: docs/index.md §13.2.1 (exhaustive built-in table), §13.2.3 (prohibited capabilities — "dynamic import of modules other than those explicitly listed").
  • Flagged in PR #103 (issue #93 / ADR-2035) review by Rui Hu, Nit #7: explicitly out of scope for that PR since ADR-2035 D-1 fixes the built-in table as unchanged; this issue is the dedicated follow-up.
  • Related: ADR-2035's read_file/write_file sandbox-root confinement (issue #93) is undermined as a hard boundary while this gap remains open — closing this issue is what turns that confinement from defense-in-depth into an actual limit.

Subtasks

  • Create the companion TDD issue (Type/Testing) proving this bug, per the project's bug-fix workflow.
  • Design a restricted __import__ shim (e.g. one that permits import json and raises for every other module name) and wire it into _execute_python_code's __builtins__ dict in place of the raw __import__ reference.
  • Verify python_exec (shares the same sandbox construction) is covered by the same restriction.
  • Tests (Behave): import json still succeeds; import os (and at least one or two other representative modules) raises; the raised error is in the same category as other prohibited-name errors.
  • Tests (Robot): integration scenario confirming an inline-code body cannot reach filesystem/network/process facilities via import.
  • Update CHANGELOG with a user-facing entry.
  • If this requires clarifying spec text (e.g. §13.2.1/§13.2.3 wording), follow the ADR + versioned spec-revision procedure (docs/index.md §21).
  • Verify coverage >= 97% via nox -s coverage_report.
  • Run nox (all default sessions), fix any errors.

Definition of Done

This issue is complete when:

  • The companion TDD issue's regression test is merged and this bug issue's dependency on it is satisfied.
  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata - **Commit Message:** `fix(agents): restrict inline-code sandbox to a json-only __import__` - **Branch:** `bugfix/m1-inline-sandbox-import-restriction` (provisional — align the milestone number with the milestone assigned at triage) ## Background and context `ToolAgent._execute_python_code` (`cleveractors.agents.tool`) builds the inline-code sandbox's `__builtins__` dict per §13.2.1's "Restricted Built-ins for Inline Code" table. That table is documented as exhaustive (§13.2.1: "the following built-in facilities MUST be available to it, and no other built-in facilities MAY be exposed") and does not list `__import__`. §13.2.3 additionally prohibits, without qualification, "performing arbitrary I/O against the filesystem, network, or system" and "dynamic import of modules other than those explicitly listed" for both sandboxes defined by the standard. Despite this, `_execute_python_code`'s `safe_globals["__builtins__"]` dict includes `"__import__": __import__` — a direct reference to the real, unrestricted import machinery — with the accompanying comment "Allow imports for json." Because `__import__` is the actual mechanism Python's `import` statement invokes, an inline-code body can write `import os`, `import subprocess`, `import socket`, etc. and reach exactly the facilities (`os`, `io`, `pathlib`, filesystem, network, process control) that §13.2.1's table and §13.2.3's prohibitions were written to keep out. This predates ADR-2035 (issue #93) and was flagged during that PR's review (#103) as pre-existing and explicitly out of scope for that change (ADR-2035 D-1 fixes the built-in table as unchanged), but it is a live security gap: while `__import__` remains, the inline-code sandbox is not a real security boundary — it is advisory at best. The new ADR-2035 `read_file`/`write_file` helpers' sandbox-root confinement is consequently only a convenience / defense-in-depth control today, not a hard limit, since any inline body can bypass it entirely via `import os; os.open(...)` or equivalent. ## Current behavior Inline code (a `type: tool` agent's `code:` body, or the `python_exec` built-in tool, which shares the same sandbox construction) can execute `import os` (or any other module) successfully, then use that module's full API — including arbitrary filesystem access, network access, and subprocess execution — with no restriction. Only the name `json` is meant to be reachable via import per the spec's intent, but nothing currently stops any other module name. ## Expected behavior Only `json` (the one module §13.2.1 names) is importable via an `import` statement inside inline code; importing any other module raises the same category of error inline code already gets for referencing any other undefined/prohibited name. Per §13.2.1, this MUST hold for the inline-code sandbox; §13.2.2 (the expression sandbox) is unaffected since it never exposed `__import__` to begin with. ## Acceptance criteria - [ ] `import json` (and the pre-bound `json` local name already relied on by the §4.5.2 canonical example) continues to work unchanged inside inline code. - [ ] `import os` (and any other non-`json` module) inside inline code raises an error — a category consistent with how `_execute_python_code` already reports prohibited-name access (see its `NameError` → `ExecutionError` translation), not a bare Python traceback. - [ ] The fix does not add any new name to the exposed `__builtins__` table beyond what §13.2.1 already lists (`json` stays the only module-shaped facility). - [ ] `python_exec` (the `exec_python`-gated built-in tool, which shares `_execute_python_code`) is restricted identically — no separate bypass path. - [ ] The §13.2.2 expression sandbox (`transform.fn`, bridge predicates) is unaffected — it already has no `__import__` and gains none. - [ ] No existing Behave/Robot scenario that relies on `import json` inside inline code regresses. ## Supporting information - Origin: `src/cleveractors/agents/tool.py`, `ToolAgent._execute_python_code`, `safe_globals["__builtins__"]["__import__"]`. - Spec: `docs/index.md` §13.2.1 (exhaustive built-in table), §13.2.3 (prohibited capabilities — "dynamic import of modules other than those explicitly listed"). - Flagged in PR #103 (issue #93 / ADR-2035) review by Rui Hu, Nit #7: explicitly out of scope for that PR since ADR-2035 D-1 fixes the built-in table as unchanged; this issue is the dedicated follow-up. - Related: ADR-2035's `read_file`/`write_file` sandbox-root confinement (issue #93) is undermined as a *hard* boundary while this gap remains open — closing this issue is what turns that confinement from defense-in-depth into an actual limit. ## Subtasks - [ ] Create the companion TDD issue (Type/Testing) proving this bug, per the project's bug-fix workflow. - [ ] Design a restricted `__import__` shim (e.g. one that permits `import json` and raises for every other module name) and wire it into `_execute_python_code`'s `__builtins__` dict in place of the raw `__import__` reference. - [ ] Verify `python_exec` (shares the same sandbox construction) is covered by the same restriction. - [ ] Tests (Behave): `import json` still succeeds; `import os` (and at least one or two other representative modules) raises; the raised error is in the same category as other prohibited-name errors. - [ ] Tests (Robot): integration scenario confirming an inline-code body cannot reach filesystem/network/process facilities via `import`. - [ ] Update CHANGELOG with a user-facing entry. - [ ] If this requires clarifying spec text (e.g. §13.2.1/§13.2.3 wording), follow the ADR + versioned spec-revision procedure (`docs/index.md` §21). - [ ] Verify coverage >= 97% via `nox -s coverage_report`. - [ ] Run `nox` (all default sessions), fix any errors. ## Definition of Done This issue is complete when: - The companion TDD issue's regression test is merged and this bug issue's dependency on it is satisfied. - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The commit is submitted as a PR to `master`, reviewed, and merged.
CoreRasurae added the
State
Unverified
Type
Bug
Priority
Critical
MoSCoW
Must have
labels 2026-08-04 18:11:51 +00:00
CoreRasurae added
State
Verified
and removed
State
Unverified
labels 2026-08-04 18:30:17 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: cleveragents/cleveractors-core#107