system: OPERATIONAL
← back to all hacks
AGENTS CRITICAL NEW

An incomplete eval() sandbox in Langroid lets a prompt run host code

Langroid's earlier fix for a TableChatAgent code-injection flaw left an opt-in path where an eval() sandbox forgets to strip Python built-ins — reopening unauthenticated remote code execution.

2026-07-10 // 6 min affects: langroid, llm-agent-frameworks, pandas-eval

What is this?

On July 6, 2026, a critical advisory landed for Langroid, a popular open-source Python framework for building LLM agent applications. Its TableChatAgent lets a model answer questions about a tabular dataset by writing a pandas expression that the agent then evaluates. The flaw, tracked in the GitLab and GitHub advisory databases, lets an attacker who can influence that model output reach unauthenticated remote code execution on the host. It is scored CVSS 10.0 — the maximum — because the impact escapes the component’s own security scope.

What makes this one instructive is its history. The same agent had a code-injection issue patched back in May 2025, which “sanitizes input by default.” The new finding is an incomplete-mitigation case: an opt-in evaluation path reopened the hole with a sandbox that only looked safe.

How it works

TableChatAgent evaluates model-produced expressions with pandas’ eval(). When the more permissive evaluation mode is enabled, the code tries to sandbox execution by handing Python’s built-in eval() an empty local namespace — the intuition being “no variables, no danger.” The problem is that emptying locals does nothing about globals: if the globals mapping does not explicitly remove __builtins__, the Python interpreter silently re-inserts the entire built-in namespace at evaluation time.

That single omission hands the expression back everything that matters — including the import machinery — so model-authored text can reach os.system and run shell commands as the process. Because the agent feeds LLM output straight into this evaluator, any input that steers the model (a poisoned document, a hostile row in a shared dataset, a user of a public-facing bot) becomes a path to code execution.

# CWE-94 anti-pattern: an "empty" sandbox that isn't one
eval(expr, {}, {})          # globals={} -> Python re-injects __builtins__
eval(expr, {"__builtins__": {}}, {})   # only THIS actually strips them

There is no operational payload here. The lesson is structural: emptying locals is not a sandbox. Stripping __builtins__ is the minimum, and even that is not a robust boundary — Python has well-documented escapes back to __builtins__ through object attribute chains. Treat “we run a restricted eval()” as a red flag, not a control.

Why it matters

This is the recurring failure mode of agent frameworks: the model is a text generator, but the framework wires that text to an action — here, arbitrary Python. The trust boundary that should sit between “the model suggested this” and “the host executed this” is missing, so influencing the model’s output is equivalent to influencing the shell.

The incomplete-fix angle matters too. The first patch made the safe behavior the default, but left a more-powerful mode reachable by a flag. Teams that flipped that flag for capability inherited a maximum-severity RCE without a new “unsafe” warning at the call site. Opt-in power is exactly where regressions hide, because the people enabling it rarely re-audit the safety the flag turns off.

Defenses

Upgrade. The issue is fixed in Langroid 0.65.2; move off any earlier version and pin it in your lockfile.

Never eval/exec model-generated code. Model output that becomes code is untrusted input by definition. If you can avoid executing it at all, do — prefer a constrained, non-code interface (a structured query DSL, an allowlisted set of parameterized operations) over “let the model write Python we run.”

If you must evaluate expressions, don’t trust eval(). Use a purpose-built restricted evaluator such as an AST-walking interpreter (e.g. asteval) or numeric-only engines (numexpr) that never expose imports or attribute access. Passing {} as locals is not a boundary; even {"__builtins__": {}} is only a speed bump.

Sandbox the whole agent, not just the call. Run the process in a disposable container or VM with no credentials, no cloud tokens, a read-only workspace, and outbound network denied by default. Then an evaluator escape lands somewhere with nothing worth stealing and nowhere to phone home.

Audit your “unsafe” flags. Grep your configuration for evaluation, full_eval, trust_remote_code, and similar opt-ins. Every one of them should have an owner who can explain what protection it disables and why the risk is accepted.

Scan dependencies. Automated dependency-advisory checks in CI would have flagged this the day it published. Make advisory scanning a gate, not a dashboard.

Status

ItemDetail
AffectedLangroid TableChatAgent and VectorStore code evaluation (full_eval path)
Root causeeval() with empty locals but un-scrubbed __builtins__ in globals (CWE-94)
ImpactUnauthenticated remote code execution on the host; CVSS 10.0 (scope changed)
Affected versionsAll versions before 0.65.2
Fixed in0.65.2
ReferencesCVE-2026-54769 / GHSA-q9p7-wqxg-mrhc; prior incomplete fix CVE-2025-46724 / GHSA-jqq5-wc57-f8hj (0.53.15, May 2025)
In the wildNo public proof-of-concept or known exploitation reported at disclosure

This article summarizes publicly disclosed, patched research and contains no operational attack instructions.

Sources