Resumen
strands-agents-tools, la colección de herramientas para agentes de IA del proyecto Strands Agents (reportada al programa de divulgación de vulnerabilidades de AWS), protege las acciones peligrosas con un aviso en el que una persona debe aprobar lo que el agente va a hacer. El problema es que ese aviso se construía así, en utils/user_input.py:35:
response = await session.prompt_async(HTML(f"{prompt} "))
HTML() de prompt_toolkit no muestra el texto tal cual: lo interpreta como marcado. Si un argumento de una herramienta contiene etiquetas, el terminal las aplica en vez de enseñarlas. Con eso se puede ocultar una parte de lo que el operador está aprobando, mientras que la cadena completa, incluida la parte invisible, es la que se ejecuta.
El investigador combinó este fallo con un segundo: en la herramienta use_computer, el argumento app_name es un texto libre (Optional[str], sin lista de valores permitidos) que llega sin sanear a tres puntos donde se ejecutan comandos del sistema. El resultado es ejecución de comandos en la máquina del agente después de que el operador apruebe un aviso de apariencia inofensiva.
El reporte se probó contra la versión en desarrollo (commit 1dd0465, del 30-sep-2026), posterior a las versiones 0.8.0 y 0.8.2.
No es el mismo fallo que CVE-2026-18733
HackerOne muestra CVE-2026-18733 en la ficha, pero el investigador insiste en que es otro problema. Ese CVE (GHSA-mqvc-p852-wf8x) trataba del parámetro non_interactive de la herramienta shell, que permitía saltarse el aviso; se arregló en la 0.8.0 tocando shell.py, python_repl.py y load_tool.py. Aquí la causa es distinta: el aviso existe y se muestra, pero se puede falsear porque se interpreta el marcado de un dato no confiable. non_interactive ni siquiera aparece en use_computer.py.
Cómo se oculta el texto
Se confirmaron dos formas de volver invisible un fragmento usando el propio analizador de prompt_toolkit (to_formatted_text):
- La etiqueta
<hidden>, que genera la secuencia SGR\x1b[0;8m(texto oculto). <style fg="#1e1e1e" bg="#1e1e1e">, que produce38;5;234;48;5;234: mismo color de letra y de fondo.
Al pasar un aviso así a la función real get_user_input() y responder y, la función devolvía 'y': la revisión humana quedaba anulada.
Dónde acaba ejecutándose app_name
En use_computer.py hay tres destinos:
línea 687 (Windows) — destino A: ejecución directa en shell
subprocess.run(f"start {actual_app_name}", shell=True, ...)
línea 747 (macOS) — destino D: inyección en AppleScript -> do shell script
script = f'tell application "{app_name}" to activate'; subprocess.run(["osascript","-e",script], ...)
línea 769 (Windows) — destino B: inyección de sentencias en PowerShell
f"...AppActivate('{app_name}')" -> powershell -Command
El aviso de consentimiento (hacia la línea 1003) se arma con locals(), que incluye app_name en bruto, y se pinta con el HTML() vulnerable. Además, no hace falta usar la acción open_app: el bloque de enfoque automático (hacia la línea 1033) lleva diez acciones —click, type, key_press, key_hold, hotkey, drag, scroll, scroll_to_bottom, screenshot y analyze_screen— a focus_application(app_name), que desemboca en los destinos B y D.
Otras herramientas afectadas por el mismo aviso
El investigador revisó todos los sitios que usan ese aviso:
use_computer.py:1003(app_name): ocultación demostrada con ejecución real y captura de bytes; ejecución de comandos por los tres destinos.load_tool.py:211(name/path): ocultación demostrada con captura de bytes; ejecución de código arbitrario.handoff_to_user.py:183(message): ocultación demostrada con el analizador; permite falsear lo que se pide aprobar al usuario.use_aws.py:449/454(operation_name): ocultación demostrada con el analizador; permite disfrazar operaciones de AWS que modifican datos o son sensibles.- No afectadas:
shell,python_repl,file_write,environmentycron(su aviso es un texto fijo, sin datos interpolados), yeditor,memoryyhttp_request(el valor interpolado se valida antes contra una lista de valores permitidos, o se pasa a mayúsculas).
handoff_to_user y use_aws no tienen un destino de ejecución: el investigador las cita para completar el cuadro, pero no cuentan para la gravedad de la ejecución de comandos.
Pasos de reproducción
El investigador adjuntó un script autónomo, uc_repro.py, que solo necesita prompt_toolkit (el resto de dependencias pesadas se sustituyen por versiones simuladas). La cadena de ataque es esta:
- El agente lee contenido no confiable que lleva una inyección de prompt indirecta. Esa inyección hace que el modelo llame a
use_computercon unapp_namepreparado. - El aviso de consentimiento pinta ese valor con
HTML(), y la parte maliciosa queda oculta. - El operador ve algo normal —en la captura del reporte,
app_name: Calculator, conStart-Process calcescondido— y aprueba cony. - El destino correspondiente ejecuta la cadena completa en bruto, con los privilegios del proceso del agente.
Lo que demuestra el script:
- Destino A: ejecución de comandos (se crea un fichero marcador) en dos máquinas.
- Destinos B y D: la carga llega sin cambios a los argumentos del intérprete; en el destino D también se crea el marcador.
- El flujo de salida del aviso contiene los bytes de ocultación (SGR 8 o letra igual al fondo) y la función de consentimiento devuelve
y.
El propio investigador aclara los límites de sus pruebas: el destino A se ejecutó forzando la rama system=="windows" en Linux y en WSL, no en un Windows nativo; los destinos B y D se demostraron a nivel de argumentos del intérprete; y handoff_to_user y use_aws solo se probaron con el analizador, porque no ejecutan nada. Tampoco usó BYPASS_TOOL_CONSENT=true, que fija el operador, para inflar la gravedad.
Impacto
- Ejecución de comandos arbitrarios en la máquina del agente, con el usuario del proceso: lectura, escritura y ejecución sobre secretos locales, código fuente, claves y cualquier servicio de red accesible desde allí.
- Si el agente tiene credenciales de AWS (variables de entorno,
~/.awso los endpoints de rol de instancia, tarea o ejecución: IMDS de EC2, rol de tarea de ECS, Lambda o Bedrock AgentCore), un solo comando inyectado basta para sacar las credenciales temporales de IAM. El daño se extiende entonces a todo lo que permita ese rol. - No hace falta acceso de red a la máquina: cualquier recurso envenenado que el agente lea sirve como punto de entrada.
- En equipos de desarrollo o de CI, los tokens de publicación o del repositorio quedan expuestos, con riesgo para la cadena de suministro.
En un laboratorio con un endpoint de credenciales simulado, un único comando inyectado obtuvo las credenciales de ese endpoint y copió ~/.aws/credentials. Eran valores de prueba, sin ninguna cuenta ni rol reales de AWS.
El investigador lo puntuó con CVSS 3.1 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H = 8,8 (7,8 con AV:L cuando el agente es local). No lo considera crítico porque siempre hace falta que el operador apruebe el aviso (UI:R) y no se cruza ninguna frontera de privilegios. HackerOne lo muestra con severidad media. El reporte cita CWE-78, CWE-116 y CWE-451, y el boletín de seguridad de AWS 2026-072-AWS.
Remediación
El reporte figura como resuelto, pero no detalla el parche aplicado. Lo que propuso el investigador:
- En
open_application, quitarshell=Truey pasar un vector de argumentos:
subprocess.run(["cmd","/c","start","",app_name])
- En
focus_application, pasarapp_namecomo dato al intérprete (porargvdeosascripto con-ArgumentListen PowerShell) y añadir una lista de aplicaciones permitidas. - En
user_input.py, pintar el aviso como texto plano o conFormattedText(que escapa solo), no usar nuncaHTML()con cadenas que contengan datos no confiables, y rechazar caracteres de control o de marcado en cualquier valor que se muestre para aprobar. Esto arregla de una vez las cuatro herramientas afectadas. - Tratar la decisión de consentimiento como un control de seguridad, con la misma neutralización de la entrada que los destinos que protege.
Qué aprender de este caso
- Si una herramienta pide confirmación humana, revisa cómo se dibuja el aviso: funciones como
prompt_toolkit.HTML(), códigos ANSI o Markdown renderizado pueden ocultar texto. Lo que se aprueba tiene que mostrarse escapado, tal cual, o la aprobación no vale nada. - En agentes con herramientas, cualquier argumento que el modelo rellene (
app_name, rutas, nombres) puede venir de una inyección de prompt indirecta. Trátalo como entrada de un atacante aunque haya un humano en medio. - Busca f-strings que acaben en
shell=True, enosascript -eo enpowershell -Command: concatenar un argumento en un script de otro lenguaje es inyección de comandos aunque se use una lista ensubprocess.run. Pásalo como argumento separado y limítalo con una lista de valores permitidos. - Para medir el alcance, mira qué otras acciones llegan a la misma función peligrosa: aquí diez acciones distintas pasaban por
focus_application, no soloopen_app.