Un sandbox eval() incompleto en Langroid permite que un prompt ejecute código
El primer parche de Langroid para un fallo de inyección de código en TableChatAgent dejó un modo opcional donde el sandbox de eval() olvida quitar los built-ins de Python, reabriendo la ejecución remota de código sin autenticación.
¿Qué es esto?
El 6 de julio de 2026 se publicó un aviso crítico para Langroid, un popular framework Python de código abierto para construir aplicaciones basadas en agentes LLM. Su TableChatAgent permite que un modelo responda preguntas sobre un conjunto de datos tabular escribiendo una expresión de pandas que el agente evalúa después. El fallo, registrado en las bases de avisos de GitLab y GitHub, permite que un atacante capaz de influir en esa salida del modelo logre ejecución remota de código sin autenticación en el host. Se le asigna un CVSS de 10.0 — el máximo — porque el impacto escapa del ámbito de seguridad del propio componente.
Lo instructivo de este caso es su historia. El mismo agente ya tuvo un problema de inyección de código parcheado en mayo de 2025, que «sanea las entradas por defecto». El nuevo hallazgo es un caso de mitigación incompleta: un modo de evaluación opcional reabrió la brecha con un sandbox que solo parecía seguro.
Cómo funciona
TableChatAgent evalúa las expresiones producidas por el modelo con el eval() de pandas. Cuando se activa el modo de evaluación más permisivo, el código intenta aislar la ejecución entregando al eval() interno de Python un espacio de nombres local vacío — con la idea de «sin variables, sin peligro». El problema es que vaciar los locals no cambia nada en los globals: si el diccionario de globals no elimina explícitamente __builtins__, el intérprete de Python reinyecta en silencio todo el espacio de nombres de las funciones integradas en el momento de la evaluación.
Esa única omisión devuelve a la expresión todo lo que importa — incluida la maquinaria de importación —, de modo que un texto redactado por el modelo puede alcanzar os.system y ejecutar comandos de shell con la identidad del proceso. Como el agente envía la salida del LLM directamente a ese evaluador, cualquier entrada que oriente al modelo (un documento envenenado, una fila hostil en un conjunto de datos compartido, el usuario de un bot expuesto al público) se convierte en una vía hacia la ejecución de código.
# Antipatrón CWE-94: un sandbox «vacío» que no lo es
eval(expr, {}, {}) # globals={} -> Python reinyecta __builtins__
eval(expr, {"__builtins__": {}}, {}) # solo ESTO los elimina de verdad
Aquí no hay ningún payload operativo. La lección es estructural: vaciar los locals no es un sandbox. Quitar __builtins__ es el mínimo, y ni siquiera eso constituye una frontera robusta — Python tiene fugas bien documentadas que regresan a __builtins__ mediante cadenas de atributos de objetos. Trate «ejecutamos un eval() restringido» como una señal de alarma, no como un control.
Por qué importa
Este es el modo de fallo recurrente de los frameworks de agentes: el modelo es un generador de texto, pero el framework conecta ese texto con una acción — aquí, Python arbitrario. La frontera de confianza que debería separar «el modelo sugirió esto» de «el host ejecutó esto» no existe, así que influir en la salida del modelo equivale a influir en el shell.
El ángulo del parche incompleto también cuenta. El primer parche convirtió el comportamiento seguro en el predeterminado, pero dejó accesible un modo más potente mediante un indicador. Los equipos que lo activaron para ganar capacidad heredaron una RCE de severidad máxima, sin una nueva advertencia «unsafe» en el punto de llamada. La potencia opcional es justo donde se esconden las regresiones, porque quienes la activan rara vez reauditan la seguridad que el indicador desactiva.
Defensas
Actualice. El problema está corregido en Langroid 0.65.2; abandone cualquier versión anterior y fíjela en su lockfile.
Nunca use eval/exec sobre código generado por el modelo. La salida de un modelo que se convierte en código es, por definición, una entrada no confiable. Si puede evitar ejecutarla, hágalo — prefiera una interfaz restringida y no basada en código (un DSL de consultas estructurado, un conjunto de operaciones parametrizadas en lista blanca) antes que «dejemos que el modelo escriba Python que ejecutamos».
Si debe evaluar expresiones, no confíe en eval(). Use un evaluador restringido específico, como un intérprete que recorre el AST (por ejemplo asteval) o motores puramente numéricos (numexpr) que nunca exponen importaciones ni acceso a atributos. Pasar {} como locals no es una frontera; ni siquiera {"__builtins__": {}} pasa de ser un reductor de velocidad.
Aísle todo el agente, no solo la llamada. Ejecute el proceso en un contenedor o VM desechable, sin credenciales, sin tokens de nube, con un espacio de trabajo de solo lectura y la red saliente denegada por defecto. Así, un escape del evaluador aterriza donde no hay nada que robar ni nadie a quien contactar.
Audite sus indicadores «unsafe». Busque en su configuración las opciones de evaluación, full_eval, trust_remote_code y otros opt-ins. Cada uno debe tener un responsable capaz de explicar qué protección desactiva y por qué se acepta el riesgo.
Analice sus dependencias. Comprobaciones automáticas de avisos de seguridad en CI habrían señalado esto el mismo día de su publicación. Haga del análisis de avisos una barrera, no un panel.
Estado
| Elemento | Detalle |
|---|---|
| Afectado | Evaluación de código de TableChatAgent y VectorStore de Langroid (modo full_eval) |
| Causa raíz | eval() con locals vacíos pero __builtins__ sin eliminar de los globals (CWE-94) |
| Impacto | Ejecución remota de código sin autenticación en el host; CVSS 10.0 (ámbito cambiado) |
| Versiones afectadas | Todas las versiones anteriores a 0.65.2 |
| Corregido en | 0.65.2 |
| Referencias | CVE-2026-54769 / GHSA-q9p7-wqxg-mrhc; parche incompleto previo CVE-2025-46724 / GHSA-jqq5-wc57-f8hj (0.53.15, mayo de 2025) |
| En circulación | Sin PoC público ni explotación conocida en el momento de la divulgación |
Este artículo resume investigación divulgada públicamente y ya parcheada, y no contiene instrucciones de ataque operativas.
Sources
- → https://advisories.gitlab.com/pypi/langroid/CVE-2026-54769/
- → https://github.com/langroid/langroid/security/advisories/GHSA-q9p7-wqxg-mrhc
- → https://github.com/langroid/langroid/security/advisories/GHSA-jqq5-wc57-f8hj
- → https://www.thehackerwire.com/cve-2026-54769-langroid-is-a-framework-for-building-large-language-model/