Agentic AI · BraivIQ AI Engineering Playbook
Agent Code Sandbox Deep Dive: Why AST Allowlists Fail And What To Run Instead
An agent code sandbox has to stop code from reaching the host, and an AST allowlist cannot do that on its own. PraisonAI's advisory of 29 October 2026, CVE-2026-61438 fixed in 4.6.78, shows why: its validator allowed imports, calls and attribute access and listed os as an allowed module, so a workflow step running os.system passed every check. This deep dive explains how Python allowlists break, when a narrow evaluator is enough, and when agent-written code needs a separate container with no network, a non-root user and a person approving it first.
Published · Updated · 11 min read · By BraivIQ Engineering
Key takeaways
- PraisonAI CVE-2026-61438 (published 29 October 2026, CVSS 7.3, fixed in 4.6.78) allowed import, call and attribute nodes and listed os as an allowed module, so os.system passed validation.
- Removing builtins does not make Python safe. Attribute access alone can walk from any object to dangerous ones.
- For formulas, use a narrow evaluator that allows arithmetic on named values and nothing else.
- For anything more, run code in a separate container with no network, a non-root user and time and memory limits. Inspect 0.3.286 (28 October 2026) moved its Docker file reads to the container's default user for the same reason.
- A person should approve new agent-written code before it touches real data.
An agent code sandbox is any arrangement that lets an agent run code without that code reaching your systems. Validation is not isolation. If agent-written Python runs in the same process as your application, the only safe programs are ones so narrow that you can list every allowed operation. Everything else belongs in a separate container with no network and a person approving it.
What did the PraisonAI advisory show?
The advisory published on 29 October 2026 (CVE-2026-61438, CVSS 7.3, fixed in praisonai 4.6.78) describes JobWorkflowExecutor._exec_inline_python(), which validated YAML workflow script steps by checking every AST node against an allowlist and supplying a restricted __builtins__ without __import__, exec, eval or open. The advisory says the allowed module list explicitly included os, and the AST allowlist permitted Import, ImportFrom, Call and Attribute nodes. A step containing import os and os.system therefore passed every check.
Why does an AST allowlist make a poor agent code sandbox?
- **Calls and attributes are enough.** Once Call and Attribute nodes are allowed, code can reach methods on any object it can name, and Python objects link to each other in many ways.
- **Builtins are not the only door.** Removing __import__ from builtins does not help if an import statement or an allowed module already exposes os.
- **The allowlist grows.** Each new feature request adds a node type or module, and every addition widens what code can reach.
- **Same process, same privileges.** A validator bug runs code with your application's credentials and network access.
What does a narrow evaluator look like?
safe_eval.py"""Why an AST allowlist is not a sandbox, and what a narrow evaluator looks like instead.
CVE-2026-61438 (PraisonAI, fixed in 4.6.78, published 29 October 2026): the workflow sandbox allowed
Import, Call and Attribute nodes and listed 'os' as an allowed module, so `import os; os.system(...)`
passed validation. Python 3.14 standard library only. The demo payload is harmless.
"""
from __future__ import annotations
import ast
import operator
NAIVE_ALLOWED = (ast.Module, ast.Import, ast.Expr, ast.Call, ast.Attribute, ast.Name, ast.Load, ast.alias,
ast.Assign, ast.Store, ast.Constant)
def naive_validate(code: str) -> bool:
return all(isinstance(node, NAIVE_ALLOWED) for node in ast.walk(ast.parse(code)))
OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv}
def evaluate(expr: str, names: dict[str, float]) -> float:
"""Arithmetic on named numbers only. No calls, attributes, imports, subscripts or comprehensions."""
def walk(node: ast.AST) -> float:
if isinstance(node, ast.Expression):
return walk(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return float(node.value)
if isinstance(node, ast.Name) and node.id in names:
return names[node.id]
if isinstance(node, ast.BinOp) and type(node.op) in OPS:
return OPS[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return -walk(node.operand)
raise ValueError(f"not allowed: {type(node).__name__}")
return walk(ast.parse(expr, mode="eval"))
if __name__ == "__main__":
payload = "import os\nos.getcwd()" # harmless here, but os.system would pass the same check
print("naive validator accepts os import:", naive_validate(payload))
print("notional x price:", evaluate("qty * price - fees", {"qty": 2500, "price": 12.345, "fees": 4.5}))
for bad in ["__import__('os')", "qty.__class__", "[x for x in ()]"]:
try:
evaluate(bad, {"qty": 1})
except ValueError as exc:
print("refused:", bad, "->", exc)Python 3.14.4, standard library only. Run once as shown. The demo payload is harmless.
The naive validator, built on the same node types the advisory lists, accepts a program that imports os. The narrow evaluator computes "qty * price - fees" from named values and refuses a call to __import__, an attribute lookup and a comprehension. It accepts only constants, known names, four arithmetic operators and unary minus. That is enough for most formulas an operations agent proposes, such as a notional or a fee check.
When does agent code need a real container?
As soon as the code needs loops, libraries or files. Run it in a separate container with outbound networking disabled, a non-root user, read-only mounts for inputs, and limits on CPU, memory and time. Return values, not objects. Inspect 0.3.286, released on 28 October 2026, made a related change: Docker read_file() now uses the container's default execution user rather than Docker's privileged copy API, and file reads and writes in sandboxes enforce user permissions.
What breaks in production?
- **Feature creep in the evaluator.** Every new operator needs a test that it cannot reach anything else. Say no to function calls.
- **Containers with network by default.** Disable outbound traffic explicitly and test it from inside the container.
- **Writable host mounts.** A mount that lets code write to the host undoes the container. Mount inputs read-only.
- **Unreviewed code reaching real data.** New agent-written code should run first on sample data, and a person should approve it before production use.
What are the alternatives to running agent code at all?
| Option | Good fit | Trade-off |
|---|---|---|
| Typed tools instead of code | Most operations agents | The agent can only do what your tools allow, which is the point |
| Narrow expression evaluator | Formulas and checks | Limited, and that limit is the safety |
| Container per run | Analysis and data preparation | Startup time and infrastructure |
| Managed code execution from a provider | Teams without container platforms | Data leaves your network, check where it runs |
Our playbook on running agents on a restricted network covers the network side, and AI agent code in Python covers approvals. Our blog explains AI agent security for non-technical colleagues.
Where should a team start?
Search your code for exec, eval and any home-made validator, and replace each with typed tools, a narrow evaluator or a container. Our AI developers in London can review the result with your team, and the 14-day Proof Run builds one agent with these limits from day one. Our services page lists the workflows we build.
Frequently asked questions
Is an AST allowlist a safe Python sandbox?
Not on its own. If the allowlist permits calls, attributes or imports, code can usually reach dangerous objects. CVE-2026-61438 in PraisonAI, published on 29 October 2026, is a recent example.
How do you safely evaluate a formula written by an AI agent?
Use an evaluator that parses the expression and allows only constants, named values and basic arithmetic, refusing everything else. Do not pass the string to eval or exec.
Which PraisonAI version fixes CVE-2026-61438?
praisonai 4.6.78, according to the advisory published on 29 October 2026.
References
- GitHub Advisory Database, "PraisonAI vulnerable to remote code execution via broken AST sandbox (CVE-2026-61438)", 29 October 2026. https://github.com/advisories/GHSA-26mh-57q7-jfvr
- UK AI Security Institute (GitHub), "Inspect CHANGELOG, release 0.3.286", 28 October 2026. https://github.com/UKGovernmentBEIS/inspect_ai/blob/main/CHANGELOG.md
- Python Software Foundation, "ast: Abstract Syntax Trees (Python 3.14 documentation)", 2026. https://docs.python.org/3.14/library/ast.html