Four attributes left out of PraisonAI's subprocess blocklist
Python sandboxes built from an attribute blocklist have a characteristic failure: the list is a description of what the author remembered, and the interpreter offers more than that. CVE-2026-39888 in PraisonAI is a precise account of four names that were missed.
The record
The CVE entry describes PraisonAI as a multi-agent teams system. Before version 1.5.115, execute_code() in praisonaiagents.tools.python_tools defaults to sandbox_mode="sandbox", which runs user code in a subprocess wrapped with a restricted __builtins__ dictionary and an AST-based blocklist.
The problem is the blocklist inside the subprocess wrapper. The blocked_attrs list in python_tools.py contains only 11 attribute names, while the direct-execution path blocks more than 30. Six of those were named in the project's own advisory. The CVE record narrows to the four that matter for escape: __traceback__, tb_frame, f_back and f_builtins, all absent from the subprocess list.
The CVSS 3.1 score is 9.9 with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H. Scope is changed, because the escape leaves the intended sandbox boundary.
The chain
Python exceptions carry their own object graph. Raising and catching an exception in Python produces an object with a __traceback__ attribute pointing at a traceback object. That object has a tb_frame pointing at the frame in which the exception was raised. A frame has f_back pointing at the calling frame. A frame also has f_builtins, the builtins dictionary that frame resolves globals against.
Each of those is a legitimate introspection feature. Together they are a path from a caught exception back to the interpreter's real builtins dictionary, the one the wrapper tried to replace with a restricted copy.
Once a reference to the real builtins dictionary exists inside user code, the restriction is over. The advisory describes retrieving exec from that dictionary and calling it through a non-blocked variable name. Name-based filtering fails against that indirection, because the blocklist matches attribute names and the escape reaches a callable by indexing a dictionary with a string.
Why a shorter list survived
Two execution paths existed: direct execution and subprocess execution. The direct path had a thorough blocklist. The subprocess path had a shorter one, apparently because the outer wrapper was assumed to carry the security weight: the subprocess was thought to be the isolation boundary, and the inner list only needed to stop casual misuse.
The premise failed in one specific way. A subprocess provides process-level isolation, not a restricted Python environment. It has its own full interpreter, its own builtins, and the same introspection features. Wrapping it with a restricted __builtins__ reproduces the appearance of a sandbox on top of a real interpreter, and the surface between them is the exception object.
Implications
Agent platforms execute model-generated code by design. The security question is not whether the code is trusted, it is whether a successful prompt injection that produces code can reach the host. In this case, the escape yields the environment the subprocess runs in, and the CVE record explicitly frames the outcome as exfiltrating all environment secrets along with arbitrary code execution.
For deployments, that means the environment variables of the agent process are part of the exposure, and they typically contain model API keys, database passwords and cloud credentials.
Remediation
- Upgrade to 1.5.115 or later. Confirm the installed distribution package rather than the project version in a lockfile, since the vulnerable code lives in the tool module.
- Do not rely on attribute blocklists as a boundary. The compensating control is process isolation with a real container: no host mounts, no shared network namespace with the control plane, an unprivileged user, a read-only root filesystem, and no credentials in the environment.
- Move secrets out of the environment. Even with a correct sandbox, model-generated code that runs near credentials is a risk. Fetching them per call from a broker with a short-lived token changes the value of a successful escape.
- Monitor what the agent runtime spawns. A tool that is supposed to execute arithmetic producing a network connection, a file write outside its workspace, or a child process is a finding.
Detection notes
Process and filesystem telemetry from the agent's container is more useful than log inspection, because the attack produces ordinary Python behaviour. A read of /proc/self/environ, a connection to a metadata endpoint, or a write to a directory the tool has no reason to touch are all visible at that layer.
Review the tracebacks your platform stores. Exception data captured for observability often contains frame references and local variables. A system that stores full tracebacks for every agent error is keeping a copy of whatever the model-generated code touched, which is both a forensic asset and a data handling problem.
The lesson that generalises
When the same policy is implemented twice, the weaker copy decides the security property. A sandbox made of names is a sandbox that knows only the names someone listed, and Python has spent decades providing more ways to walk a frame than any blocklist will enumerate.
References
- CVE.org record for CVE-2026-39888: https://www.cve.org/CVERecord?id=CVE-2026-39888
- NVD entry for CVE-2026-39888: https://nvd.nist.gov/vuln/detail/CVE-2026-39888
- GHSA-2xv2-w8cq-5gxw, PraisonAI CodeAgent advisory: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-2xv2-w8cq-5gxw
Top comments (0)