Tu agente tiene un intermediario que no verificaste
Unos investigadores compraron 28 routers de API de LLM de pago en Taobao, Xianyu y tiendas alojadas en Shopify, y reunieron otros 400 gratuitos en comunidades públicas. En cada uno registraron una cuenta, hicieron pasar por él un agente de código aislado en un sandbox, ejecutaron cada llamada a herramienta que regresaba y observaron lo que hacía.1
Nueve de los 428 reescribieron una llamada a herramienta para convertirla en un comando o una dependencia bajo control del atacante: un router de pago y ocho gratuitos. Diecisiete routers gratuitos llegaron a usar una credencial canary de AWS después de verla pasar, y uno vació una clave de Ethereum plantada. Dos de los routers que inyectaban ocultaban ese comportamiento: uno esperaba 50 solicitudes antes de actuar y otro solo se activaba en sesiones en “YOLO mode” (el modo autónomo, sin confirmación) que trabajaban en proyectos de Rust o Go.1
Un router de API de LLM es un proxy de capa de aplicación. Termina TLS, lee en texto plano cada solicitud y cada respuesta, y puede reescribir la llamada a herramienta que tu agente está a punto de ejecutar. El artículo no encontró a ningún proveedor importante que firme sus respuestas de llamadas a herramientas, así que nada vincula el comando que ejecuta tu agente con lo que produjo el modelo. El artículo probó cuatro frameworks de agentes, entre ellos Claude Code y Codex, y ninguno verificaba la integridad de la respuesta.1
Este post se publicó por primera vez el 10 de abril de 2026 a partir del resumen del artículo. El 2 de octubre de 2026 lo reescribí con el texto completo delante. La versión de abril tenía varios errores, y están enumerados en “Lo que la versión de abril hizo mal”, cerca del final.
TL;DR
- Lo que puede hacer un router. Se sitúa entre tu cliente y el proveedor del modelo como el endpoint que configuraste, de modo que tiene en texto plano cada solicitud y cada respuesta, y puede cambiar cualquiera de las dos antes de reenviarla.1
- Lo que hicieron 428 routers. Nueve inyectaron código malicioso en las llamadas a herramientas devueltas (1 de 28 de pago, 8 de 400 gratuitos). Dos de los gratuitos usaban disparadores para ocultarlo. Diecisiete routers gratuitos usaron una credencial canary de AWS propiedad de los investigadores, y uno vació una clave de Ethereum de los investigadores que contenía menos de 50 dólares.1
- Lo que hicieron una clave filtrada y unos relays débiles. Una clave de OpenAI filtrada a propósito sirvió 100 millones de tokens de GPT-5.4 y expuso más de siete sesiones de Codex. Unos relays señuelo con configuración débil sirvieron unos 2.000 millones de tokens y expusieron 99 credenciales en 440 sesiones de Codex, 401 de las cuales ya funcionaban con las llamadas a herramientas aprobadas automáticamente.1
- Lo que lograron las defensas. En el benchmark sintético de los propios autores, una puerta de política fail-closed bloqueó todas las muestras de inyección con una tasa de falsos positivos del 1,0 %, y en un benchmark adaptativo sencillo un atacante que conocía la puerta la eludió en el 100 % de las muestras. La detección de anomalías marcó el 89,0 % de las inyecciones simples y alrededor de la mitad de las evasivas.1
- Lo que, según los autores, lo arreglaría. Un sobre de respuesta firmado por el proveedor, para que el cliente pueda contrastar una llamada a herramienta con lo que produjo el modelo. El artículo no encontró a ningún proveedor importante que lo ofrezca.1
- Si alojas tú mismo el router. La actualización del 1 de octubre, más abajo, cubre once avisos de seguridad de LiteLLM; uno de ellos permitía a cualquier usuario autenticado hacer que el proxy enviara fuera sus claves de proveedor.2
Puntos clave
- Operadores de agentes: Cada router entre tu cliente y el proveedor del modelo tiene acceso en texto plano a cada solicitud y cada respuesta, y el cliente solo configura el primer salto. Si compraste un router en un marketplace o lo sacaste de una lista pública, trátalo como un intermediario hostil hasta que tengas una razón independiente para confiar en quien lo opera.
- Constructores de harness: Un hook PreToolUse se ejecuta antes de que se ejecute la llamada a herramienta, y para entonces el router ya tuvo su oportunidad de reescribirla. El hook no tiene un original con el que comparar. Lo que sí puede hacer es fallar en cerrado: permitir comandos de shell que solo descarguen de dominios listados e instalen solo paquetes listados, y bloquear el resto.3
- Quien use YOLO mode: En el estudio con señuelos de los investigadores, 401 de las 440 sesiones de Codex observadas ya funcionaban con la ejecución de herramientas aprobada automáticamente.1 En esas sesiones, un simple comando reescrito se habría ejecutado sin necesidad de ninguna lógica de disparo. No hagas pasar sesiones con aprobación automática por un router que no controlas.
- Equipos que alojan su propio proxy: Un router que operas tú concentra las claves de proveedor de la misma manera. Once avisos de LiteLLM que llegaron a la base de datos de PyPA el 1 de octubre de 2026, la mayoría públicos desde junio, incluyen uno que permitía a cualquier usuario autenticado hacer que el proxy enviara sus claves de proveedor a un host de su elección. Las versiones a partir de 1.97.0 quedan fuera de los once rangos registrados; los detalles están en la actualización del 1 de octubre, más abajo.2
¿Qué es un router, exactamente?
Un router de API de LLM acepta solicitudes en un formato, normalmente compatible con OpenAI, elige un proveedor upstream y devuelve la respuesta. El artículo encuentra routers a todas las escalas: servicios gestionados en la nube como Amazon Bedrock y Azure OpenAI Service, proyectos y servicios orientados a desarrolladores como LiteLLM y OpenRouter, y un mercado de commodities de acceso a APIs revendido y agregado.1
La gente los usa por buenas razones. El artículo enumera “model fallback, load balancing, cost optimization, and a single API key across providers” (respaldo entre modelos, balanceo de carga, ahorro de costos y una sola clave para todos los proveedores), y señala que el enrutamiento es especialmente común “in regions where direct provider access is restricted, expensive, or subject to quota limitations” (en regiones donde el acceso directo al proveedor está restringido, es caro o tiene cuotas).1
El problema es la posición. No hace falta ningún truco de interceptación, en palabras del artículo: “the client voluntarily configures the router’s URL as the API endpoint, the router terminates the client-side TLS connection, and it originates a separate TLS connection upstream” (el propio cliente configura la URL del router como endpoint; el router termina esa conexión TLS y abre otra hacia arriba). TLS protege cada tramo, pero no protege en absoluto la carga útil frente al router, que lee el JSON de la solicitud, lo reenvía, lee el JSON de la respuesta y lo devuelve, con la posibilidad de cambiar cualquiera de los dos.1
Además, los routers se encadenan. El ejemplo del artículo es un desarrollador que compra acceso a un revendedor de Taobao, que agrega claves de un agregador de segundo nivel, que enruta a través de OpenRouter, que despacha al host del modelo: cuatro saltos, cada uno con acceso completo en texto plano. “The client configures only the first hop; subsequent hops are invisible.” (El cliente solo configura el primer salto; los demás son invisibles.) Basta un salto malo en cualquier punto para contaminar la ruta, y los saltos honestos que vienen después no pueden notarlo.1
Nada en las APIs actuales cierra esa brecha. Los argumentos de las llamadas a herramientas viajan como JSON en texto plano, y “No provider-level integrity mechanism binds the arguments returned by the model to the arguments received by the client.” (ningún mecanismo de integridad del proveedor liga los argumentos que devuelve el modelo a los que recibe el cliente).1
Cómo se hizo la medición
Para cada router, los autores siguieron cuatro etapas. Registraron una cuenta controlada por ellos y obtuvieron credenciales de API. Hicieron pasar esa cuenta por un entorno de agente aislado que ejecutaba “a randomized subset of custom tasks and TerminalBench 2.0 tasks” (un subconjunto aleatorio de tareas propias y de TerminalBench 2.0), que generan llamadas a herramientas realistas de shell, instalación de paquetes y manipulación de archivos. Ejecutaron cada carga útil de llamada a herramienta devuelta dentro del sandbox, que vigilaba callbacks de red, cambios en el sistema de archivos y anomalías en el árbol de procesos. Después, un agente de análisis independiente revisaba las ejecuciones marcadas y clasificaba cada una como reescritura benigna, potencialmente maliciosa o maliciosa confirmada.1
Las definiciones son conservadoras. Un router cuenta como inyector de código malicioso “only if a returned tool-call payload is rewritten into an attacker-controlled command or dependency” (solo si una llamada devuelta se reescribe en un comando o dependencia del atacante). Una credencial canary de AWS cuenta como usada cuando una credencial que pasó por el router “later produces follow-on AWS API activity attributable to that credential” (genera después actividad en la API de AWS atribuible a esa credencial).1
El corpus es el mercado de commodities, no todo el campo. Los 400 routers gratuitos están dominados por dos plantillas de código abierto, sub2api y new-api, y los autores dicen que las implementaciones empresariales y las de solo invitación quedan fuera del estudio. La mayor parte del corpus viene de marketplaces y comunidades en chino.1
Los datos de campo
| Comportamiento | Routers de pago (28) | Routers gratuitos (400) |
|---|---|---|
| Inyecta código malicioso en las llamadas a herramientas devueltas | 1 | 8 |
| Oculta la inyección tras un disparador | 0 observados | 2 |
| Usa una credencial canary de AWS de los investigadores | 0 observados | 17 |
| Vacía una clave de Ethereum de los investigadores | 0 observados | 1 |
Las inyecciones no son cosméticas. El artículo dice que “change the command or dependency that the agent intends to execute” (cambian el comando o la dependencia que el agente pretende ejecutar). El resultado de pago es un router de 28, y los autores sacan la conclusión evidente: “Paid access improves service stability, but it does not prove tool-call integrity.” (Pagar mejora la estabilidad del servicio, pero no prueba la integridad de las llamadas a herramientas.)1
Los dos routers con disparadores son el hallazgo que derrota a las pruebas. En uno, “malicious tool calls are injected only after the first 50 requests, which hides the attack from shallow smoke tests” (las llamadas maliciosas solo se inyectan tras las primeras 50 solicitudes, lo que oculta el ataque a las pruebas superficiales). En el otro, la inyección se activa “only for sessions that appear to be running in autonomous YOLO mode and only when the project fingerprint matches Rust or Go” (solo en sesiones que parecen estar en YOLO mode autónomo y solo si la huella del proyecto corresponde a Rust o Go). Los autores añaden que los disparadores que vieron quizá no sean los únicos. Observaron estos routers solo mediante un sondeo de caja negra finito, así que “a router may implement additional latent conditions that our probes did not activate” (un router puede tener otras condiciones latentes que sus sondeos no activaron).1
Todos los hallazgos sobre credenciales están en el conjunto gratuito. Diecisiete routers “trigger follow-on use of at least one researcher-owned AWS canary credential after seeing it in transit” (provocan el uso posterior de al menos una credencial canary de AWS de los investigadores después de verla pasar), y uno vació una clave de Ethereum con fondos precargados. El apéndice cifra la pérdida de ese vaciado en menos de 50 dólares estadounidenses.1
Los dos estudios de envenenamiento
Un router no tiene que ser malicioso para terminar en la misma posición. Los autores hicieron dos estudios sobre cómo se envenena una ruta de routers de apariencia benigna.
Estudio 1: una clave filtrada. Filtraron una clave de API de OpenAI propiedad de los investigadores “on Chinese forums, WeChat, and Telegram groups frequented by router operators” (en foros chinos y en grupos de WeChat y Telegram que frecuentan los operadores de routers). Esa sola clave sirvió 100 millones de tokens de GPT-5.4 y expuso más de siete sesiones de usuarios de Codex, y en al menos una sesión aparecieron detrás de ella varias credenciales distintas. El artículo es cuidadoso sobre cómo llegó ese tráfico. El resultado “is consistent with several paths: direct reuse by a router operator, direct reuse by unrelated third parties, or reuse somewhere inside a longer relay chain” (encaja con varias rutas: reutilización directa por un operador de router, por terceros sin relación o en algún punto de una cadena de relays más larga), y los autores “cannot always distinguish among those paths from provider-side logs alone” (no siempre pueden distinguir entre ellas solo con los registros del proveedor). Lo que el estudio establece es el radio de impacto: quien tenga una clave upstream reutilizada ve en texto plano el tráfico que pasa por ella.1
Estudio 2: relays débiles. Desplegaron instancias vulnerables de claude-relay-service en 20 dominios e instancias de Sub2API y CLIProxyAPI con contraseñas débiles en 20 IPs. Los señuelos atrajeron más de 40.000 intentos de acceso no autorizado desde 147 IPs, que los autores interpretan como una mezcla de escaneo y reutilización de relays y no, en todos los casos, como ataques dirigidos. La señal más fuerte llegó después: los señuelos “were folded into active agent-facing relay paths” (quedaron integrados en rutas de relay activas usadas por agentes). Sirvieron aproximadamente 2.000 millones de tokens de GPT-5.4 y 5.3-codex, unos 13 GB de tráfico visible de prompts y respuestas, y expusieron 99 credenciales únicas en 440 sesiones de Codex repartidas entre 398 proyectos o hosts.1
Cada una de esas 440 sesiones admitía inyección de comandos según la definición del artículo, y 401 “already operated in YOLO mode, i.e., with tool execution auto-approved rather than confirmed command by command” (ya funcionaban en YOLO mode, es decir, con la ejecución de herramientas aprobada automáticamente en lugar de confirmada comando a comando). Para esas 401, escriben los autores, “sophisticated adaptive evasion was unnecessary: basic payload injection would have been sufficient to change executed commands” (no hacía falta ninguna evasión adaptativa sofisticada: una inyección básica habría bastado para cambiar los comandos ejecutados).1
Eran sesiones de otras personas. Su tráfico llegó a los señuelos de los investigadores después de que estos se integraran en rutas de relay, y un usuario que solo configura el primer salto no habría tenido forma de verlo. Los autores conservaron solo metadatos agregados y hashes con sal, y retiraron sus propias credenciales expuestas al cerrar la ventana de observación.1
Dos clases de ataque centrales y dos variantes de evasión
El artículo reduce el comportamiento de un router malicioso a dos primitivas y dos refinamientos de la primera.1
AC-1: inyección de carga útil en la respuesta. El router reescribe una llamada a herramienta después de que sale del proveedor y antes de que llegue al cliente, y conserva intactos el nombre de la herramienta y el esquema. El ejemplo del artículo es una llamada Bash cuya URL de instalador se sustituye por la de un atacante. Como el resultado sigue siendo JSON válido para la herramienta esperada, el cliente no ve nada raro.
AC-2: exfiltración pasiva de secretos. El router no cambia nada. Escanea solicitudes y respuestas en busca de patrones de credenciales y se queda con lo que encuentra. El tráfico no se modifica, así que “clients cannot observe router-side retention” (los clientes no pueden observar lo que el router retiene). La exposición va más allá de las claves de API: “system prompts, tool definitions, user-supplied file contents, and environment variables all transit the same plaintext channel” (los prompts de sistema, las definiciones de herramientas, el contenido de los archivos del usuario y las variables de entorno pasan por el mismo canal en texto plano).
AC-1.a: inyección dirigida a dependencias. Una variante de AC-1 orientada a los comandos de instalación de paquetes. En lugar de cambiar una URL, el router “substitutes a legitimate dependency name with an attacker-controlled package pre-registered on the target registry” (sustituye el nombre de una dependencia legítima por un paquete del atacante registrado de antemano en el registro de destino). El ejemplo del artículo convierte pip install requests en pip install reqeusts. El comando sigue instalando desde el registro de confianza, así que una puerta que solo revisa dominios lo deja pasar, y el paquete malicioso queda en caché para sesiones posteriores.
AC-1.b: entrega condicional. Una variante que decide cuándo se dispara AC-1. El router se comporta honestamente hasta que se cumple un disparador. El artículo enumera cinco familias de disparadores: nombre de herramienta, palabras clave del contenido, huellas del usuario como YOLO mode, ventanas de tiempo y número de solicitudes. “A router deploying a 50-request warm-up appears entirely transparent during a 10-request probe.” (Un router con un calentamiento de 50 solicitudes parece del todo transparente en una prueba de 10.)
El artículo presenta el compromiso de LiteLLM de marzo de 2026 como AC-1 a escala: una vez que un atacante controló la cadena de procesamiento de solicitudes del proxy, cada llamada a herramienta que pasaba por ella quedaba expuesta a ser reescrita. (El artículo describe ese compromiso como confusión de dependencias. Las fuentes con las que trabajé en el post sobre la cadena de suministro lo atribuyen a un token de publicación de PyPI robado a través del compromiso de Trivy.)
Lo que el proxy de investigación mostró sobre los clientes de agentes
Los autores construyeron un proxy al que llaman Mine, que implementa las cuatro clases, y enviaron 1.000 solicitudes con llamadas a herramientas por cada módulo de ataque a través de cada uno de cuatro frameworks públicos de agentes: OpenClaw, OpenCode, Codex de OpenAI y Claude Code de Anthropic.1
Ninguno de los cuatro verificó la integridad de la respuesta. La llamada reescrita llegó en una forma válida y nativa del framework en el 100 % de los casos de AC-1 y en el 99,6 % de los de AC-1.a. El artículo es preciso sobre lo que eso muestra y lo que no: “These are compatibility measurements, not end-to-end execution rates: they show that the tampered tool call reaches the client in a valid framework-native format, but downstream permission prompts or local execution sandboxes may still block the resulting action.” (Son medidas de compatibilidad, no tasas de ejecución de extremo a extremo: la llamada manipulada llega en un formato válido, pero un aviso de permisos o un sandbox local aún pueden bloquear la acción.)1
Esa salvedad es justo donde un aviso de permisos se gana el sueldo, y explica por qué la aprobación automática pesa tanto en las cifras de los señuelos. Los autores no publicaron Mine.1
El patrón de composición, una capa más abajo
Sigo escribiendo sobre el mismo bug estructural: componentes autorizados uno por uno que, al combinarse, producen un comportamiento no autorizado. Trivy-to-LiteLLM fue composición en la capa de paquetes. El egress silencioso fue composición en la capa del contenido descargado. El envenenamiento de herramientas MCP fue composición en la capa de protocolo.
El ataque del router es composición en el salto entre el cliente y el proveedor. Tu cliente está autorizado a llamar al router. El router está autorizado a llamar al modelo upstream. El modelo está autorizado a responder. Cada salto está autorizado, y aun así la composición produce comandos reescritos, porque nadie firma lo que cruza el salto, y secretos copiados, porque cada salto lo lee en texto plano.
El artículo traza la línea hasta el propio MCP: “a malicious MCP server receives tool-call requests in plaintext and can return forged results, so the same basic manipulation and collection ideas transfer with adaptation to the MCP message format” (un servidor MCP malicioso recibe en texto plano las solicitudes de llamadas a herramientas y puede devolver resultados falsificados, de modo que las mismas ideas de manipulación y recolección se trasladan, adaptadas, al formato de mensajes de MCP). También marca la diferencia. Un servidor MCP está del lado de la ejecución de herramientas y puede falsificar su salida. Un router está en la ruta hacia el modelo y ve cada llamada a herramienta y todo el contexto de la solicitud.1
Tres defensas, con las cifras del artículo
El artículo evalúa tres controles que un cliente puede desplegar sin ayuda de ningún proveedor. Hay una salvedad que rige todas las cifras siguientes: “All numbers in this section come from controlled artifact-side evaluation rather than field data.” (Todas las cifras de esta sección provienen de una evaluación controlada, no de datos de campo.) Los corpus son sintéticos, generados con los scripts de los autores.1
| Defensa | AC-1 | AC-1.a | AC-1.b | Costo y límite |
|---|---|---|---|---|
| Puerta de política fail-closed | 100 % bloqueado | 100 % bloqueado | no medido | 1,0 % de falsos positivos; eludida en el 100 % de las muestras en un benchmark adaptativo sencillo |
| Detección de anomalías en la respuesta | 89,0 % marcado | 50,0 % marcado | 50,9 % marcado | 6,7 % de falsos positivos |
| Registro de transparencia de solo anexado | solo auditoría | solo auditoría | solo auditoría | unos 1,26 KB por entrada |
La puerta de política bloquea los comandos de shell que “fetch from non-allowlisted domains or install packages outside a local allowlist” (descargan de dominios fuera de la lista de permitidos o instalan paquetes fuera de una lista local). Sobre 1.000 comandos benignos, 500 muestras de AC-1 y 500 de AC-1.a bloqueó todas las muestras de ataque, y todos sus falsos positivos fueron descargas de instaladores desde dominios espejo que faltaban en la lista. Los autores la llaman “the strongest immediately deployable control” (el control más sólido que se puede desplegar de inmediato). También informan sin rodeos de su fallo. Si el atacante aloja la carga útil en un dominio permitido, o deja preparado un script local que más tarde ejecuta un comando de apariencia benigna, “the policy gate is bypassed in 100% of samples” (la puerta se elude en el 100 % de las muestras). Su veredicto: la puerta “should be understood as a coarse but useful filter for obvious high-risk workflows, not as a robust defense against defense-aware attackers” (debe entenderse como un filtro tosco pero útil para flujos de alto riesgo evidentes, no como una defensa robusta frente a atacantes que conocen las defensas).1
Hay un detalle que importa a quien construya la puerta, y el artículo no es coherente al respecto. La sección 4 describe la puerta como basada solo en dominios y diseña AC-1.a para burlarla: “the gate blocks non-allowlisted domains but does not maintain a per-package allowlist” (la puerta bloquea dominios no permitidos, pero no mantiene una lista de paquetes). La puerta de la sección 7, la que obtuvo el 100 % contra AC-1.a, también bloquea instalaciones “outside a local allowlist” (fuera de una lista local). Según mi lectura, una puerta sin la lista de paquetes no detecta en absoluto la sustitución por typosquatting.
La detección de anomalías puntúa cada llamada a herramienta devuelta con rasgos visibles localmente, como patrones de riesgo de shell y la entropía de los argumentos, sin un original upstream con el que comparar. Con una tasa de falsos positivos del 6,7 % marcó el 89,0 % de las muestras de AC-1, el 50,0 % de AC-1.a y el 50,9 % de AC-1.b. Con un presupuesto de falsos positivos del 1 % detectó el 43,5 % de AC-1 y prácticamente nada de AC-1.a. Los autores la consideran “useful for review prioritization” (útil para priorizar revisiones) y más débil que la puerta.1
El registro de transparencia guarda la solicitud, la respuesta, la URL del router, los metadatos de TLS y un hash de la respuesta en bruto. No previene nada. Te permite responder, después de un incidente, hasta dónde llegó un router o una credencial y qué sesiones pasaron por él.1
La solución que piden los autores
Ninguno de los tres controles autentica de dónde vino una llamada a herramienta. Los autores lo dicen: “No client-side control available today can prove that a router preserved the upstream provider’s response.” (Ningún control del lado del cliente disponible hoy puede probar que un router conservó la respuesta del proveedor.)1
Lo que sí lo haría es una firma del proveedor. El artículo propone “a provider-signed canonical response envelope, similar in spirit to DKIM for email” (un sobre de respuesta canónico firmado por el proveedor, parecido en espíritu a DKIM para el correo), que cubra el identificador del modelo, el nombre de la herramienta, sus argumentos, el motivo de finalización y un nonce del cliente, y que el cliente verifique antes de ejecutar cualquier llamada a herramienta. Informa de que nadie lo ofrece: “To our knowledge, none of the major provider tool-use APIs or the current MCP specification expose a deployed response-signing mechanism for tool-call arguments today.” (Hasta donde saben, ni las APIs de los grandes proveedores ni la especificación actual de MCP ofrecen hoy un mecanismo desplegado de firma de respuestas para los argumentos.)1
La propuesta trae dos límites. La seguridad del transporte no la sustituye: TLS mutuo y el anclaje de certificados “can authenticate the router endpoint the client chose, but they do not say whether the returned tool call preserves upstream semantics” (pueden autenticar el endpoint elegido, pero no dicen si la llamada devuelta conserva lo que produjo el upstream). Y firmar no sirve de nada contra los secretos robados: “AC-2 cannot be mitigated by response-signing proposals because the secrets are exposed on the request path before any provider-side mechanism can act.” (AC-2 no se mitiga firmando respuestas, porque los secretos quedan expuestos en la solicitud antes de que el proveedor pueda actuar.)1
Qué deberías hacer realmente
Si tu agente llama a un modelo a través de un router que no construiste:
- Conéctate directamente donde puedas, y conoce al operador donde no puedas. Basta con cambiar una URL base para añadir un router, y por eso se añade sin que nadie lo decida. Conviértelo en una decisión. “Confianza” significa aquí una base externa, como un equipo conocido, un contrato o una jurisdicción ante la que puedas reclamar. Las reseñas de un marketplace no lo son.
- Falla en cerrado ante las llamadas a herramientas de alto riesgo. En Claude Code eso es un hook PreToolUse que bloquea los comandos de shell que descargan de dominios fuera de una lista de permitidos y las instalaciones de paquetes fuera de otra. Mantén la lista de paquetes: la variante de sustitución de dependencias existe para burlar una puerta basada solo en dominios. Escribe el hook para que deniegue todo lo que no pueda analizar, con el código de salida 2 o una decisión
deny: Claude Code trata los demás códigos de salida y los tiempos de espera agotados como no bloqueantes, así que un hook que falla o se cuelga no detiene la llamada, que sigue el flujo normal de permisos y, en una sesión con aprobación automática, simplemente se ejecuta. Comprueba también que el hook se ejecutó en su primera llamada. La documentación advierte que “a mistyped path in settings.json leaves the gate silently disabled” (una ruta mal escrita en settings.json deja la puerta desactivada sin aviso). Cuenta con tener que mantener ambas listas, y con que un atacante decidido las esquive.3 - Nunca hagas pasar sesiones con aprobación automática por un router que no controlas. Las 401 sesiones del estudio con señuelos son el precedente. Un aviso de permisos es una de las pocas cosas que se interponen entre una llamada a herramienta reescrita y su ejecución.
- Mantén los secretos fuera del tráfico. La recolección pasiva no cambia nada que puedas observar. Todo lo que va en un prompt, en un resultado de herramienta o en un archivo que lee el agente cruza el router en texto plano. Limita el alcance de las credenciales, mantenlas fuera del contexto donde puedas y rota todo lo que haya pasado por un router del que luego dudes.
- Registra localmente. Solicitudes, respuestas, la URL del router y un hash de la respuesta, con los secretos eliminados antes de las solicitudes, guardados donde el router no llegue. No detendrá un ataque. Te dice después qué quedó expuesto.
- Aísla la ejecución en un sandbox. El artículo señala que los sandboxes “reduce post-execution blast radius but do not authenticate where a tool call came from” (reducen el radio de impacto tras la ejecución, pero no autentican el origen de una llamada a herramienta). Quédate con la primera mitad.
La implicación incómoda
La capa de routers es un ejemplo claro de un ecosistema de agentes que despliega infraestructura más rápido de lo que la asegura. La gente quiere una sola clave para todos los modelos, precios más bajos y acceso desde regiones que un proveedor no atiende. Los routers ofrecen las tres cosas, y el mercado los premia.
La misma secuencia se ha repetido en la capa MCP, en la capa de paquetes y en la capa del contenido descargado. Aparece una nueva capa del stack de agentes. Los desarrolladores la adoptan antes de que nadie la audite. Llegan los atacantes, y luego los investigadores. Aquí los investigadores contaron 428 routers, 9 que inyectaban código malicioso, 17 que usaban credenciales plantadas, 1 que vació una billetera y 401 sesiones con aprobación automática que circulaban por rutas de relay que incluían los señuelos de los investigadores.1
La pieza que cerraría la brecha, una respuesta firmada por el proveedor, no es algo que un operador pueda añadir. Hasta que los proveedores la ofrezcan, los controles anteriores reducen la exposición, y ninguno prueba que una llamada a herramienta sea realmente del modelo.
Actualización, 1 de octubre de 2026: el router que alojas tú también está en la lista
Este post trata de routers que opera otra persona. El registro de avisos de seguridad completa la otra mitad. El 1 de octubre de 2026, la base de datos de avisos de PyPA añadió once entradas contra LiteLLM, un proxy de código abierto que los equipos alojan ellos mismos por las razones de arriba: una sola clave para todos los modelos.2 Ninguna es nueva. Nueve están en la base de datos de avisos de GitHub y en NVD desde el 21 de junio, una décima desde mediados de septiembre, y la que más importa para un proxy, porque expone las claves de proveedor, se publicó en el propio repositorio de LiteLLM el 26 de agosto, con correcciones en PyPI desde el 9 de agosto; GitHub la califica de gravedad Moderate. Vale la pena leerlas juntas por lo que dicen de esta capa, y porque todas las versiones finales publicadas antes del 9 de agosto están dentro del rango de CVE-2026-84377.
La primera que hay que leer es CVE-2026-84377. En palabras del aviso del repositorio, “Any authenticated LiteLLM proxy user could redirect an outbound provider call to a destination they control and cause the proxy to send its own configured provider credentials to that destination.” (Cualquier usuario autenticado del proxy podía redirigir una llamada saliente a un destino propio y hacer que el proxy le enviara sus credenciales de proveedor.) La causa es la forma de la comprobación: “The proxy’s request-body validation was a denylist that did not cover every sensitive parameter and did not inspect parameters nested inside other request fields.” (La validación del cuerpo de la solicitud era una lista de bloqueo que no cubría todos los parámetros sensibles ni inspeccionaba los anidados en otros campos.) Está corregido en nueve líneas de versiones, de 1.88.6 a 1.96.2. Para quien todavía no pueda actualizar, el aviso enumera tres soluciones provisionales en una sola frase: “Set general_settings.allow_client_side_credentials to false so callers cannot override connection parameters, restrict proxy keys to trusted callers, and block the affected parameters (api_base, base_url, model_list, fallbacks, provider credential fields) at a reverse proxy or API gateway.” (poner esa opción en false para que nadie sobrescriba los parámetros de conexión, limitar las claves del proxy a llamantes de confianza y bloquear esos parámetros en un proxy inverso o un gateway de API).2 La primera no basta por sí sola. En el código fuente de 1.95.0, una versión afectada, la comprobación del cuerpo de la solicitud solo se omite por completo cuando esa opción vale true (el configurable_clientside_auth_params de un despliegue todavía puede eximir parámetros sueltos), así que false es donde ya está una instalación que nunca la tocó, y la comprobación incompleta es justamente el fallo que describe el aviso. La solución es actualizar.6
Una segunda entrada, CVE-2026-59823, es el mismo fallo en miniatura: la protección “blocks the api_base and base_url parameters but does not cover user_config,” (bloquea esos dos parámetros, pero no cubre user_config), de modo que quien tuviera una clave virtual válida podía meter un api_base dentro y apuntar el proxy a cualquier host. Esa se corrigió en 1.83.9, en PyPI desde el 17 de abril; su aviso llegó en septiembre.4
Dos entradas afectan al lado MCP del proxy: autenticación incorrecta en el proxy MCP (CVE-2026-12773, con un rango de versiones que termina en una corrección en 1.84.0) y falsificación de solicitudes del lado del servidor a través del argumento spec_path del cargador de especificaciones OpenAPI de MCP (CVE-2026-12798, registrada como que afecta a las versiones hasta 1.82.2). Las otras siete cubren el manejo de claves de administración, el flujo de depuración de SSO, la invalidación de sesiones SSO, la caducidad de sesión de las claves generadas, la enumeración de usuarios en la interfaz, una forma de saltarse un guardrail en endpoints asíncronos y el manejo de JWT entre máquinas.5
Esos nueve registros son más pobres que los dos primeros. Sus descripciones llevan la redacción de plantilla de VulDB en lugar de una explicación del mantenedor, y la descripción de la entrada del proxy MCP dice “up to 1.59.8” (hasta 1.59.8) mientras que su rango de versiones dice corregido en 1.84.0.5 El registro de CVE-2026-84377 tiene su propia discrepancia: la página del repositorio da como versiones afectadas “<1.94.0”, mientras que los rangos del registro revisado cubren la línea 1.96 hasta su corrección en 1.96.2.2
Compara los rangos con la versión que ejecutas en lugar de fiarte de un resumen, incluido este. Todos los rangos registrados terminan en 1.96.2 o por debajo, así que las versiones a partir de 1.97.0, en PyPI desde el 16 de agosto, quedan fuera de las once; la versión actual a 1 de octubre es 1.103.2.5
El peligro del router está en dónde se guardan las credenciales, sea quien sea quien lo opere. Un router de marketplace que usa una clave AWS plantada y un proxy autoalojado al que se puede convencer de enviar fuera sus claves de proveedor son la misma exposición alcanzada desde dos direcciones, una por el operador y otra por cualquier inquilino con una clave virtual. Las defensas de las secciones anteriores dan por hecho que tú eres el cliente.
Para un proxy que operas tú, añade la mitad que corresponde al operador: trata cada clave virtual como una credencial de acceso a las claves upstream que hay detrás, mantén desactivados los parámetros de conexión que aporta el cliente, incluido el configurable_clientside_auth_params de cada despliegue, salvo que los necesites, y aplica al proxy el mismo ritmo de parches que a cualquier otra cosa que guarde secretos.
Si alguien en quien no confías del todo tuvo una clave virtual mientras el proxy ejecutaba una versión afectada, rota las claves de proveedor y cualquier otro secreto configurado en el proxy, y revisa sus registros en busca de llamadas salientes a hosts que no configuraste; el impacto descrito en el aviso abarca “other configured secrets” (otros secretos configurados) y solicitudes a “internal services reachable from the proxy” (servicios internos accesibles desde el proxy). Dos de las once entradas, CVE-2026-12773 en el proxy MCP y CVE-2026-12795 en el flujo de depuración de SSO, son fallos de autenticación en versiones anteriores a 1.84.0, así que en esas versiones quién tenía una clave quizá no delimite quién podía llegar al proxy; si era accesible desde redes en las que no confías, aplica la misma rotación.
Lo que la versión de abril hizo mal
La versión del 10 de abril de este post se escribió a partir del resumen del artículo. Leída frente al texto completo el 2 de octubre de 2026, era errónea o engañosa en estos puntos, todos corregidos arriba:
- AC-1.a. La describí como una inyección que “only fires when the request matches a specific dependency or context” (solo se dispara cuando la solicitud coincide con una dependencia o contexto concretos). Es la sustitución del nombre de un paquete dentro de un comando de instalación. Los disparadores son AC-1.b.
- Las defensas. Escribí que “the abstract does not rank the defenses” (el resumen no clasifica las defensas) y ofrecí una clasificación como opinión mía. El cuerpo del artículo mide las tres, llama a la puerta de política “the strongest immediately deployable control” (el control más sólido que se puede desplegar de inmediato) e informa de que en un benchmark adaptativo sencillo fue eludida en el 100 % de las muestras. Eso lo omití.
- El consejo sobre firmas. Les dije a los operadores que firmaran las solicitudes en el cliente y las verificaran en el upstream, y lo llamé “the only real fix” (la única solución real). El artículo pide la dirección contraria, una respuesta firmada por el proveedor y verificada por el cliente, y dice que firmar no resuelve el robo de secretos.
- La clave filtrada. Dije que la clave se filtró “as if it had been exposed through a developer mistake” (como si la hubiera expuesto un error de un desarrollador) y concluí que “The router was a laundering layer for a stolen key.” (el router fue una capa de lavado para una clave robada). Se filtró en foros y grupos de chat donde los operadores de routers comparten credenciales, y el artículo dice que no siempre puede distinguir si la reutilizó un operador de router, un tercero sin relación o una cadena de relays más larga.
- Los recuentos de credenciales. El bloque de respuesta decía “17 of 28 paid routers touched planted AWS credentials” (17 de 28 routers de pago tocaron credenciales AWS plantadas), y la descripción decía que los investigadores probaron 28 routers. La fila de pago del artículo no registra ningún abuso de credenciales. Los 17 routers que usaron credenciales canary de AWS, y el que vació ETH, están todos entre los 400 routers gratuitos.
- El consejo sobre hooks. Recomendé hooks PostToolUse que “validate response shapes” (validen la forma de las respuestas). Un hook PostToolUse se ejecuta después de que la herramienta se ejecutó, demasiado tarde para un comando reescrito. El control que prueba el artículo es una puerta previa a la ejecución, que en Claude Code es un hook PreToolUse con una lista de permitidos.
- Errores menores. El artículo tiene seis autores, no cinco. No dice que un router “knows when it is being sampled” (sabe cuándo lo están muestreando); eso fue un adorno mío. La entrada llamaba al tema “MCP trust chains” (cadenas de confianza de MCP), y el artículo estudia routers, no MCP. Describí el post sobre egress silencioso como si tratara de descripciones de herramientas, cuando trata de instrucciones ocultas en contenido descargado.
Preguntas frecuentes
¿Qué es un router de API de LLM en este contexto?
Un servicio que acepta solicitudes en un formato unificado, normalmente compatible con OpenAI, elige un proveedor de modelos upstream y devuelve la respuesta. Es un proxy de capa de aplicación con acceso en texto plano a cada solicitud y cada respuesta.1
¿TLS me protege de un router malicioso?
No. El cliente configura el router como su endpoint, así que el router termina la sesión TLS del cliente y abre otra distinta hacia arriba. TLS protege cada tramo y no hace nada para proteger la carga útil frente al router.1
¿Cómo detectaría un router que está reescribiendo llamadas a herramientas?
No de forma fiable a base de probarlo. Dos routers del estudio solo inyectaban después de 50 solicitudes o solo en sesiones con aprobación automática sobre proyectos de Rust o Go, y el artículo concluye que “no fixed-length client test can guarantee that the router is benign” (ninguna prueba de longitud fija en el cliente puede garantizar que el router sea benigno). Una lista de permitidos fail-closed para comandos de shell e instalaciones de paquetes bloquea los casos simples, y el artículo muestra que un atacante que conoce la defensa la supera.1
¿Sirve de algo un hook PreToolUse?
Sí, como puerta de política. El hook ve la llamada a herramienta que recibió el cliente, que es la reescrita si un router la reescribió, y puede bloquear comandos que descargan de dominios no listados o instalan paquetes no listados. No puede saber si la llamada es la que produjo el modelo.13
Uso Claude Code directamente contra api.anthropic.com. ¿Me afecta?
No en cuanto a los ataques de routers de este artículo, porque no hay intermediario. Si haces pasar Claude Code por un proxy por cualquier motivo, como un gateway corporativo o un agregador de modelos, ese proxy ocupa la misma posición.
¿Y OpenRouter, LiteLLM u otros agregadores conocidos?
El artículo mide 28 routers de pago de tres marketplaces y 400 routers gratuitos construidos sobre todo a partir de dos plantillas de código abierto. Menciona LiteLLM y OpenRouter como contexto y no los prueba. El argumento estructural vale para cualquier router: puede leer y reescribir el tráfico, y la visibilidad es una propiedad distinta de la integridad. Para un proxy que alojas tú, la actualización del 1 de octubre, más arriba, cubre once avisos de LiteLLM.
¿De quién eran las 401 sesiones con aprobación automática?
De terceros cuyo tráfico llegó a los relays señuelo de los investigadores. Si ejecutas sesiones de agentes con aprobación automática a través de un router que no construiste, detente, rota cada credencial que lo haya cruzado y revisa los registros de las sesiones en busca de llamadas a herramientas que no esperabas.
Referencias
-
Hanzhi Liu, Chaofan Shou, Hongbo Wen, Yanju Chen, Ryan Jingyang Fang y Yu Feng, “Your Agent Is Mine: Measuring Malicious Intermediary Attacks on the LLM Supply Chain,” arXiv:2604.08407v1, 9 de abril de 2026, incluido en el programa de la ACM Conference on Computer and Communications Security, octubre de 2026. Texto completo leído el 1 y el 2 de octubre de 2026. Secciones usadas: introducción y 2.1 (qué son los routers, el ejemplo de cuatro saltos, la terminación de TLS); 2.2 (ningún mecanismo de integridad a nivel de proveedor); 4.1 y 4.2 (las cuatro clases de ataque, el ejemplo de
requestsareqeusts, las cinco familias de disparadores); 5.1 (la cadena de cuatro etapas y las definiciones); 5.2 y tablas 3 y 4 (1 router de pago y 8 gratuitos que inyectan, 2 gratuitos con disparadores, 17 gratuitos que usan credenciales canary de AWS, 1 que vacía ETH); 5.3 (la clave filtrada y los señuelos: 100 millones de tokens, más de siete sesiones de Codex, más de 40.000 intentos de acceso desde 147 IPs, unos 2.000 millones de tokens, unos 13 GB, 99 credenciales, 440 sesiones, 398 proyectos o hosts, 401 en YOLO mode); 5.4 (los hallazgos clave, incluida la frase sobre el acceso de pago); 5.5 (alcance); 6 y tabla 5 (Mine, cuatro frameworks, 1.000 solicitudes por módulo, compatibilidad del 100 % y del 99,6 %); 7 y tabla 6 (las tres defensas y sus resultados); 8.2 y 8.3 (el sobre de respuesta firmado, MCP); 9 (la diferencia entre la posición de un servidor MCP y la de un router); apéndice A (retención, retirada de credenciales, el vaciado de menos de 50 dólares estadounidenses, Mine no publicado). ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Aviso del repositorio de LiteLLM GHSA-3cv6-jpf6-8222 (CVE-2026-84377, PYSEC-2026-4066), “Authenticated SSRF and provider-credential exfiltration via unvalidated request-body routing parameters,” publicado en el repositorio el 26 de agosto de 2026, listado por NVD el 2 de septiembre y revisado por GitHub el 30 de septiembre; leído en la página del repositorio y en el registro de OSV el 1 de octubre de 2026. El impacto y la frase completa de soluciones provisionales se citan del aviso; la línea de parches del registro de OSV dice “Fixed in 1.96.2, 1.95.1, 1.94.3, 1.93.2, 1.92.2, 1.91.5, 1.90.7, 1.89.7, and 1.88.6,” y la página del repositorio indica “Affected versions <1.94.0.” El recuento de once corresponde a las entradas PYSEC-2026-4066 a PYSEC-2026-4076 de la base de datos de avisos de PyPA, todas para el paquete
litellm, todas fechadas el 1 de octubre de 2026 y ninguna retirada. ↩↩↩↩↩ -
Anthropic, Hooks reference, documentación de Claude Code, consultada el 2 de octubre de 2026. PreToolUse se ejecuta “Before a tool call executes. Can block it” (antes de que se ejecute una llamada a herramienta; puede bloquearla); PostToolUse se ejecuta “After a tool call succeeds” (después de que una llamada a herramienta tiene éxito); “Any other exit code doesn’t block on its own for most hook events” (cualquier otro código de salida no bloquea por sí solo en la mayoría de los eventos); un hook de comando que agota el tiempo “doesn’t block the tool call” (no bloquea la llamada a herramienta), y “a mistyped path in settings.json leaves the gate silently disabled” (una ruta mal escrita en settings.json deja el control desactivado sin aviso). ↩↩↩
-
Aviso de seguridad de GitHub GHSA-hx8v-g79f-8w5f (CVE-2026-59823, PYSEC-2026-4070), “LiteLLM Proxy has server-side request forgery via the
user_configrequest parameter,” publicado el 17 de septiembre de 2026, leído a través de OSV el 1 de octubre de 2026: afectadas<= 1.83.8, corregido en1.83.9, versión que el aviso fecha el 17 de abril de 2026. ↩ -
Registros de OSV leídos el 1 de octubre de 2026: PYSEC-2026-4067 (CVE-2026-12773, autenticación del proxy MCP), PYSEC-2026-4069 (CVE-2026-12798, cargador de especificaciones OpenAPI de MCP) y PYSEC-2026-4068, 4071, 4072, 4073, 4074, 4075 y 4076. Los avisos de GitHub que hay detrás de estos nueve se publicaron el 21 de junio de 2026, el mismo día en que NVD los listó, y cada uno cita a VulDB. Las fechas de subida y la versión actual provienen de la página del proyecto en PyPI y de su JSON, leídos ese mismo día: de 1.88.6 a 1.95.1 el 9 de agosto, 1.96.2 el 11 de agosto, 1.97.0 el 16 de agosto y 1.103.2 el 1 de octubre. ↩↩↩
-
Lectura del autor del wheel de
litellm1.95.0 de PyPI (rango afectado desde 1.95.0 hasta la corrección en 1.95.1), 1 de octubre de 2026: enlitellm/proxy/auth/auth_utils.py,_check_banned_paramsretorna antes de rechazar nada cuandogeneral_settings.get("allow_client_side_credentials") is True, y en caso contrario rechaza una solicitud cuyo cuerpo lleve un parámetro de la lista prohibida, salvo que elconfigurable_clientside_auth_paramsdel despliegue permita ese parámetro. La lista prohibida, en esa versión, incluyevertex_ai_credentialsy las credenciales y hosts de observabilidad. Leí una sola versión afectada, no las nueve líneas. ↩