El loopback no es un límite de confianza: CVE-2026-2611
El 19 de mayo de 2026, la National Vulnerability Database publicó CVE-2026-2611. La función Assistant de MLflow 3.9.0 validaba incorrectamente el origen en sus endpoints locales de /ajax-api. Una página maliciosa cargada en cualquier pestaña podía enviar solicitudes de origen cruzado al servidor local del Assistant, modificar su configuración para activar el modo de acceso completo y, después, ejecutar comandos arbitrarios mediante la integración incluida con Claude Code CLI. Huntr calificó la vulnerabilidad con CVSS 3.0 de 9,6 (CRITICAL), con el vector AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H: accesible desde la red, de baja complejidad, sin privilegios requeridos, con cambio de alcance e impacto total sobre confidencialidad, integridad y disponibilidad.1
La causa raíz no es nueva y la corrección no es exótica, pero el modelo de amenaza sí cambió. Cualquier servicio local que se enlaza a loopback y expone endpoints de mutación a una pestaña del navegador crea una superficie que cruza un límite de confianza. Cuando ese servicio controla un agente de IA capaz de leer archivos, ejecutar comandos de shell y subir código, “solo loopback” deja de funcionar como control de seguridad. CVE-2026-2611 es el caso de manual para toda una clase de vulnerabilidades que aparecerá una y otra vez a medida que más herramientas de desarrollo envuelvan agentes LLM dentro de UI web locales.
Resumen rápido
CVE-2026-2611 documenta un error de enumeración CORS incompleta en MLflow 3.9.0. La función Assistant exponía endpoints /ajax-api/ que la verificación is_api_endpoint() de MLflow no cubría, así que el middleware que bloqueaba CORS los omitía. Además, el CORSMiddleware heredado permitía allow_origins=["*"], por lo que una solicitud de origen cruzado desde evil.com podía llamar a /ajax-api/3.0/mlflow/assistant/config, cambiar el Assistant a modo de acceso completo y luego enviar comandos de shell mediante la integración incluida con Claude Code CLI.1 La corrección del proyecto original (PR #20832, “Block CORS for ajax paths”) en MLflow 3.10.0 agrega /ajax-api/ a la enumeración de rutas protegidas y restringe allow_origins a la lista permitida configurada más una expresión regular para localhost.2 Las defensas siguientes cubren la clase de agentes locales de IA, no solo MLflow.
Puntos clave
| Rol | Acción |
|---|---|
| Ingenieros de IA/ML que ejecutan MLflow | Actualiza a MLflow 3.10.0; audita cualquier otra UI local de agente de IA que escuche en loopback |
| Equipos de seguridad | Agrega “servicio local en loopback de agente LLM” a tus plantillas de modelo de amenazas; una sola brecha CORS equivale a RCE en esta clase |
| Autores de herramientas que crean UI locales de agentes | Enumera una sola vez cada prefijo de ruta de API; prohíbe allow_origins=["*"] incluso en servidores enlazados a localhost; protege los endpoints destructivos con un gesto reciente del usuario, no solo con el origen |
| Líderes de ingeniería | Aísla los agentes de programación de IA del equipo y trata su capacidad de ejecutar comandos como el radio real de impacto |
Cómo se ve la cadena de ataque
La cadena tiene cinco pasos y es corta. Cada paso usa capacidades autorizadas; lo que falla es la composición.
| Paso | Acción | Lo que controla el atacante |
|---|---|---|
| 1 | El usuario ejecuta mlflow ui localmente; el Assistant escucha en loopback (por defecto, 127.0.0.1) |
Nada todavía (servicio local) |
| 2 | El usuario abre evil.com en cualquier pestaña mientras MLflow está en ejecución |
El contenido de la página |
| 3 | evil.com envía fetch('http://127.0.0.1:5000/ajax-api/3.0/mlflow/assistant/config', { method: 'PUT', credentials: 'include', body: ... }) |
La solicitud de origen cruzado |
| 4 | MLflow acepta la solicitud porque /ajax-api/ no estaba en la enumeración de rutas con bloqueo CORS; la configuración del Assistant cambia a acceso completo |
La configuración del entorno de ejecución del Assistant |
| 5 | evil.com publica cargas útiles de comandos mediante el endpoint de chat del Assistant; MLflow invoca el Claude Code CLI local con las verificaciones de permisos omitidas; los comandos se ejecutan como el usuario |
Ejecución arbitraria de código |
El atacante no necesita instalar nada en la máquina de la víctima. Solo necesita que la víctima cargue una pestaña con la carga útil mientras mlflow ui está en ejecución. El ataque funciona desde cualquier origen porque el CORSMiddleware heredado tenía allow_origins=["*"] y el middleware de bloqueo CORS por ruta no cubría el prefijo /ajax-api/.2
La integración con Claude Code es el amplificador de privilegios. El ClaudeCodeProvider de MLflow ubica el CLI local de claude mediante shutil.which("claude") e indica a los usuarios que lo instalen con npm install -g @anthropic-ai/claude-code cuando no está presente. Cuando la bandera de configuración permissions.full_access del Assistant está activada, el proveedor extiende la invocación de CLI con --permission-mode bypassPermissions, el modo de Claude Code que suprime los avisos interactivos de permiso y permite ejecutar llamadas a herramientas sin confirmación del usuario.3 Las capacidades autorizadas del agente (shell, escritura de archivos, HTTP) se convierten en una shell remota con UX de lenguaje natural, justo lo que busca un atacante.
La causa raíz es una enumeración incompleta de rutas
Al leer el commit de corrección, el error salta a la vista. security_utils.py de MLflow tenía una constante:
API_PATH_PREFIX = "/api/"
y una verificación:
def is_api_endpoint(path: str) -> bool:
return path.startswith(API_PATH_PREFIX) and path not in TEST_ENDPOINTS
El middleware que bloqueaba CORS llamaba a is_api_endpoint() para decidir si debía aplicar validación de origen. Cualquier ruta que no empezara con /api/ pasaba como ruta no API. El Assistant introdujo /ajax-api/ para sus endpoints orientados al navegador. Nadie actualizó is_api_endpoint() para incluir el nuevo prefijo.
La corrección es un cambio de comportamiento de una sola línea en security_utils.py:2
API_PATH_PREFIX = "/api/"
AJAX_API_PATH_PREFIX = "/ajax-api/"
def is_api_endpoint(path: str) -> bool:
return (
path.startswith(API_PATH_PREFIX) or path.startswith(AJAX_API_PATH_PREFIX)
) and path not in TEST_ENDPOINTS
Además, hay una corrección paralela en fastapi_security.py que reemplaza allow_origins=["*"] en el CORSMiddleware heredado por la lista permitida configurada más una expresión regular para localhost. El cambio combinado suma 5 líneas y elimina 2 en security_utils.py, y suma 5 líneas y elimina 1 en fastapi_security.py.2
Esa es toda la vulnerabilidad. CVE-2026-2611 ocurrió porque se publicó un nuevo prefijo de endpoint sin actualizar la única función que enumera qué rutas reciben protección CORS. La superficie de ataque fue una constante de configuración olvidada.
Por qué este patrón se repetirá
El error es más antiguo que MLflow y más antiguo que los agentes de IA. Los frameworks web han tenido fallas de enumeración de rutas desde que existe la enumeración de rutas. Lo que cambió fue la consecuencia. Una UI web local que olvidaba enumerar un endpoint antes podía filtrar algunos datos de configuración o provocar una denegación de servicio. Una UI web local que olvida enumerar un endpoint mientras ejecuta un agente de programación de IA con acceso a shell filtra toda la máquina del desarrollador.
Las UI locales de agentes de IA son una categoría en crecimiento. Cualquier herramienta que envuelva un agente LLM dentro de una UI de navegador servida desde 127.0.0.1 entra en el alcance: rastreadores de experimentos con paneles de chat (la forma de MLflow), plugins de IDE que controlan un agente mediante una API en localhost, servidores en modo desarrollo con los que una extensión se comunica por HTTP, paneles web para ejecutores locales de modelos. Cada uno se enlaza a loopback. Cada uno expone endpoints de mutación a una pestaña del navegador. Cada uno ejecuta un agente con permiso para invocar shell, escribir archivos y subir cambios a Git. Todos están a un solo error de enumeración de rutas de convertirse en CVE-2026-2611.
La suposición compartida debajo de esta clase es: “la política de mismo origen del navegador protegerá el servidor en loopback porque las solicitudes legítimas también vienen de 127.0.0.1”. La suposición falla en cuanto cualquier puerta de origen cruzado (CORS, validación del encabezado Host, validación del encabezado Origin, encabezados de metadatos fetch) omite un prefijo de endpoint. El navegador no protege al servidor cuando el servidor acepta explícitamente la solicitud.
El ataque de salida silenciosa que cubrí en marzo rompe la misma suposición de confianza en la dirección contraria. La salida silenciosa oculta la salida mediante las herramientas autorizadas del agente. La mala clasificación de loopback oculta la entrada mediante las llamadas de red autorizadas del navegador. Ambos se reducen a esto: las capacidades autorizadas de un agente LLM ahora son una primitiva de ataque cuando se rompe una sola verificación de límite.
Cómo se ve realmente esta clase
La clase de vulnerabilidad tiene cuatro ingredientes. Un servicio es explotable cuando se cumplen los cuatro:
- Servidor web local enlazado a loopback o
0.0.0.0. La variante más común es solo loopback. Lo que protege a un servicio enlazado a localhost frente a una pestaña de origen cruzado son las verificaciones CORS, Host y Origin del navegador, no el enlace de red. - Endpoints de mutación alcanzables sin un gesto del usuario. Cambios de configuración, envíos de comandos, escrituras de archivos. Si un
fetch()desde un origen arbitrario puede mutar estado, el servicio es alcanzable. - Capacidad que tiene el usuario local, pero que una página web no debería tener. Leer el directorio personal, ejecutar comandos de shell, llamar a APIs de nube con credenciales guardadas, modificar archivos fuente.
- Una verificación de límite faltante. Prefijo de ruta olvidado,
allow_originscomodín, falta de validación del encabezado Host, falta de validación de Origin, falta de aplicación de Sec-Fetch-Site.
CVE-2026-2611 cumplía los cuatro. La corrección cierra el cuarto ingrediente para el prefijo /ajax-api/ en MLflow específicamente. Los ingredientes uno a tres siguen presentes en cualquier otra herramienta local de agentes de IA. El próximo CVE de esta clase se verá idéntico, pero con otro nombre de paquete.
Defensas contra la clase loopback de agentes locales de IA
El consejo estándar de CORS (configurar allow_origins con una lista específica, habilitar credenciales solo cuando haga falta) es necesario, pero no suficiente. Esta clase necesita controles más profundos.
Enumera los prefijos de ruta en un solo lugar y escribe una prueba que falle cuando aparezca un prefijo nuevo sin protección. El error de MLflow llegó porque el equipo actualizó is_api_endpoint() cuando se publicó /api/ y lo olvidó cuando se publicó /ajax-api/. Una prueba que recorra cada ruta registrada en la app FastAPI, pregunte a is_api_endpoint() si cada ruta está protegida y falle ante cualquier prefijo de ruta no reconocido habría detectado el error durante el PR. La verificación cuesta unas 20 líneas de código de prueba.
Valida los encabezados Origin y Host, no solo CORS. CORS protege a los navegadores de la respuesta del servidor. No protege al servidor de aceptar la solicitud. Los servicios locales de agentes de IA deberían rechazar solicitudes cuyo encabezado Origin no esté en la lista explícita permitida y cuyo encabezado Host no sea 127.0.0.1 o localhost (esto último para prevenir ataques de DNS rebinding). Ambas verificaciones deben fallar de forma cerrada.
Protege los endpoints destructivos con Sec-Fetch-Site: same-origin. Los navegadores envían Sec-Fetch-Site: cross-site en cualquier fetch() de origen cruzado, incluso desde evil.com hacia 127.0.0.1. La especificación Fetch Metadata Request Headers define los valores del encabezado, y Chrome, Firefox, Safari y Edge lo envían.4 Un middleware que rechace solicitudes a endpoints de mutación salvo que tengan Sec-Fetch-Site: same-origin aporta una capa adicional que no depende de recordar cada prefijo de ruta.
Aísla la capacidad de ejecución del agente de IA. Claude Code, Cursor, Aider y Continue se ejecutan por defecto con todos los privilegios del usuario porque esa es la UX más fácil. Un entorno aislado de capacidades por espacio de trabajo (bwrap con reglas explícitas de montaje en Linux; aislamiento de apps de macOS para herramientas distribuidas como paquetes .app; un contenedor por espacio de trabajo para compatibilidad multiplataforma) reduce el radio de impacto sin sacrificar el flujo principal. La mitigación agrega fricción. Esa fricción es el objetivo.
Trata los commits del agente como no confiables hasta que se revisen. Protección de ramas, revisiones obligatorias, commits firmados. El modelo de “agente de IA con acceso para hacer commits” se rompe en el momento en que un atacante compromete el entorno local del agente. La disidencia del revisor (tratada en la revisión de código con IA necesita disenso) también aplica aquí. Si el agente de IA y el revisor humano siempre están de acuerdo, el revisor humano es decorativo.
Cómo probablemente será el próximo CVE de esta clase
La pregunta interesante no es si aparecerá la próxima vulnerabilidad con forma de CVE-2026-2611. La pregunta interesante es qué paquete la publicará primero. Hay candidatos en todo el ecosistema de herramientas locales de IA: rastreadores de experimentos que envuelven un agente en su UI, plugins de IDE que hablan con un backend local de agente por HTTP, servidores en modo desarrollo que cualquier herramienta abre durante una sesión, las APIs en localhost que usan las extensiones para controlar UI de agentes. Ninguno ha publicado todavía un CVE con esta forma. Varios ya distribuyen rutas de código que se enlazan a loopback y aceptan solicitudes de origen cruzado con el navegador como puerta de confianza.
La clase es más grande que CVE-2026-2611 y el modelo de amenaza cambió. En 2026, una máquina de desarrollo aloja un agente de IA que puede invocar shell, escribir archivos, subir commits y llamar a APIs de nube. Las UI web locales que controlan esos agentes heredan los privilegios del agente mediante cualquier límite de confianza que aplique el servidor HTTP de la UI. Una mala configuración CORS en un framework web de 2020 filtraba algunos datos de configuración. Una mala configuración CORS en una UI local de agente de IA de 2026 filtra toda la máquina del desarrollador, más la capacidad de desplegar a producción.
La corrección no es un parche. La corrección es una clase de controles: enumeración explícita de rutas con pruebas, validación de encabezados con defensa en profundidad, aislamiento de capacidades y disciplina de revisión sobre commits escritos por agentes. Las herramientas que publiquen uno de esos controles sin los demás estarán publicando al sucesor de CVE-2026-2611 con otro prefijo.
Preguntas frecuentes
¿Qué es CVE-2026-2611?
CVE-2026-2611 es el identificador de la National Vulnerability Database para una falla de validación incorrecta de origen en la función Assistant de MLflow 3.9.0. La falla permite que cualquier página web haga solicitudes de origen cruzado al endpoint local /ajax-api del Assistant, modifique su configuración para activar el modo de acceso completo y luego ejecute comandos arbitrarios mediante la integración incluida con Claude Code CLI. Puntuación base CVSS 3.0: 9,6 (CRITICAL). Corregida en MLflow 3.10.0 mediante el PR #20832, “Block CORS for ajax paths”.12
¿Cuál fue la causa raíz?
La verificación is_api_endpoint() de MLflow enumeraba solo el prefijo de ruta /api/. Cuando la función Assistant introdujo endpoints /ajax-api/, nadie actualizó la enumeración. Entonces el middleware que bloqueaba CORS dejaba pasar las solicitudes de origen cruzado a /ajax-api/ sin ninguna verificación de origen. Una segunda debilidad agravó el error: el CORSMiddleware heredado permitía allow_origins=["*"], así que incluso las rutas no bloqueadas devolvían encabezados CORS permisivos a cualquier origen. La corrección agrega AJAX_API_PATH_PREFIX = "/ajax-api/" a la verificación de rutas y reemplaza allow_origins=["*"] por la lista permitida configurada más una expresión regular para localhost.2
¿Cómo sé si mi instalación de MLflow está afectada?
Ejecuta pip show mlflow | grep Version. Según el rango CPE de NVD, las versiones de MLflow >=3.9.0,<3.10.0 están afectadas. MLflow 3.10.0 (publicada el 20 de febrero de 2026) incluye la corrección. Actualiza con pip install --upgrade mlflow y reinicia cualquier proceso mlflow ui que esté en ejecución.
¿El ataque funciona sin tener instalado Claude Code CLI?
El paso de modificación de configuración funciona de todos modos. El paso de ejecución arbitraria de código requiere que Claude Code CLI esté presente en el PATH de la víctima, porque el Assistant de MLflow lo invoca mediante shutil.which("claude"). Sin CLI, el peor caso pasa a ser divulgación de información y manipulación arbitraria de configuración, no RCE. Para usuarios que configuraron el backend de Claude Code (la ruta documentada de configuración del Assistant), la exposición práctica es RCE.
¿Cómo protejo otros servicios locales de agentes de IA?
Audita los cuatro ingredientes: enlace a loopback o comodín, endpoints de mutación, capacidades a nivel de agente y cualquier verificación de límite que pueda tener brechas. Agrega listas explícitas permitidas de Origin, validación del encabezado Host contra 127.0.0.1/localhost (para derrotar DNS rebinding) y puertas Sec-Fetch-Site: same-origin en endpoints destructivos. Aísla la capacidad de ejecución del agente con bwrap, aislamiento de apps de macOS o un contenedor por espacio de trabajo. Trata los commits de Git del agente como no confiables hasta que se revisen.
-
National Vulnerability Database. “CVE-2026-2611: MLflow Assistant improper origin validation.” Publicado el 19 de mayo de 2026. Puntuación base CVSS 3.0 de 9,6 (CRITICAL), vector
CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, emitida por Huntr. Corrección incluida en MLflow 3.10.0. https://nvd.nist.gov/vuln/detail/CVE-2026-2611. Informe original: https://huntr.com/bounties/8462addd-b464-4a84-b6a2-5529604e6e5a. ↩↩↩ -
MLflow. “Block CORS for ajax paths (#20832).” Fusionado el 16 de febrero de 2026 por Tomu Hirata. Archivos modificados:
mlflow/server/fastapi_security.py(+5/-1),mlflow/server/security_utils.py(+5/-2), además de actualizaciones de pruebas. El diff reemplaza la verificación de prefijo único deis_api_endpoint()por una verificación de dos prefijos y restringe elallow_originsdelCORSMiddlewareheredado de["*"]a la lista permitida configurada más una expresión regular para localhost. https://github.com/mlflow/mlflow/commit/8f9c8a53af90842944101eb8b7d60706822c81bc. ↩↩↩↩↩↩ -
Código fuente de MLflow 3.10.0.
mlflow/assistant/providers/claude_code.py: búsquedashutil.which("claude")(líneas 268, 280, 355); indicación de instalación"Install it with: npm install -g @anthropic-ai/claude-code"(línea 286); escalamiento de modo de permisocmd.extend(["--permission-mode", "bypassPermissions"])condicionado porconfig.permissions.full_access(líneas 379-381).mlflow/assistant/config.py: valor predeterminadoPermissionsConfig.full_access: bool = False(línea 15).mlflow/server/assistant/api.py: prefijo del router/ajax-api/3.0/mlflow/assistant(línea 63); lectorGET /config(línea 261); mutadorPUT /config(línea 276), el endpoint PUT al que apunta el ataque de origen cruzado para activarfull_access. Verificado contra el sdist de MLflow 3.10.0 en PyPI (mlflow-3.10.0.tar.gz). ↩ -
W3C. “Fetch Metadata Request Headers.” Define
Sec-Fetch-Sitey sus valorescross-site,same-origin,same-site,none. Implementado por Chrome 76+, Firefox 90+, Safari 16.4+ y Edge 79+. https://www.w3.org/TR/fetch-metadata/ ↩