FlawOpen

flawopen.com/AI Security/mlflow-webhook-ssrf-cve-2026-64849

● CVE-2026-64849 · CVSS 8.6 · Высокий
Исследования · FlawOpen

CVE-2026-64849: SSRF в вебхуках MLflow

CVE-2026-64849 — SSRF без аутентификации в доставке вебхуков MLflow (высокая, CVSS 3.1: 8.6). Проверка исходного URL не ограничивала адреса перенаправлений и адрес фактического соединения; тестовый endpoint мог возвращать тело ответа целевого сервера.[1]

💡 Простыми словами (ELI5)

Представьте сотрудника, который проверил адрес на посылке, но разрешил курьеру следовать указанию о пересылке без проверки нового адреса. MLflow проверял URL вебхука до подключения, однако перенаправление и повторный DNS-запрос могли направить запрос в другое место. Исправление проверяет адрес реально подключённого узла до передачи HTTP-данных.[1], [2]

Ключевые понятия и термины

SSRF
Сервер выполняет выбранный вызывающей стороной сетевой запрос, пересекая границу доверия своей сети.[1]
Тестовый endpoint вебхука
API MLflow отправляет тестовое событие настроенному вебхуку; затронутый сервер по умолчанию возвращал вызывающему тело ответа.[1]
DNS rebinding / TOCTOU
Имя во время проверки разрешается в публичный адрес, а при подключении — в другой, непубличный адрес.[1], [2]

Связи со стандартами

Пошаговый сценарий атаки

Шаг 1

Доступный сервер MLflow принимает тест вебхука

В бюллетене описан стандартный Tracking Server без аутентификации. Плагины аутентификации и настройки развертывания меняют доступность; проверьте реальную конфигурацию. 19 августа 2026 года CISA добавило CVE в каталог Known Exploited Vulnerabilities. Это указывает на известную эксплуатацию, но не доказывает компрометацию конкретного развертывания.[1], [4], [5]

Шаг 2

Исходный URL проходит проверку

_validate_webhook_url разрешает исходное имя и проверяет адреса, но не сохраняет проверенный адрес для последующего сокета.[1]

Шаг 3

Перенаправление или повторное разрешение ведёт к другой цели

HTTP-клиент следует перенаправлениям, а соединение может заново разрешить имя. Endpoint теста возвращает тело ответа, поэтому SSRF позволяет читать ответ целиком.[1]

Шаг 4

Исправление upstream проверяет подключённый узел

PR #24258 проверяет адрес peer сокета сразу после соединения и до TLS/HTTP; сессия также отключает прокси из окружения. CVE исправлена в MLflow 3.15.0; отдельные обходы через IPv6 переходы исправлены в 3.16.0. В уведомлении freeman-bb указан как автор исходного закрытого сообщения, а AUTHENSOR — как независимый исследователь.[1], [2], [3], [6]

Исходный код: Уязвимый vs Защищённый вариант

Фрагменты показывают границу между проверкой URL и запросом, а также upstream-проверку peer в момент подключения. Запросы эксплуатации и внутренние адреса опущены. Нормализация IPv6 в 3.16.0 — более позднее отдельное усиление защиты.[2]

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
# Фрагмент mlflow/webhooks/delivery.py до PR #24258.
_validate_webhook_url(webhook.url)  # checks the initial URL only
return session.post(
    webhook.url, data=payload_bytes, headers=headers, timeout=timeout
)  # redirects follow by default; the validated IP is not pinned
БЕЗОПАСНЫЙ ИСПРАВЛЕННЫЙ ВАРИАНТ
# Фрагмент upstream-исправления с проверкой узла при подключении.
def _assert_public_peer(sock):
    if _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():
        return
    try:
        peer_ip = sock.getpeername()[0]
    except OSError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Could not determine webhook connection peer address: {exc}"
        ) from exc
    try:
        ip = ipaddress.ip_address(peer_ip)
    except ValueError as exc:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection resolved to an invalid IP address: {peer_ip!r}"
        ) from exc
    if not ip.is_global:
        sock.close()
        raise SSRFProtectionError(
            f"Webhook connection blocked: {ip} is not a public IP address. "
            "This may indicate a DNS rebinding attempt."
        )


class _SSRFProtectedHTTPConnection(HTTPConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock


class _SSRFProtectedHTTPSConnection(HTTPSConnection):
    def _new_conn(self):
        sock = super()._new_conn()
        _assert_public_peer(sock)
        return sock

adapter = SSRFProtectedHTTPAdapter(max_retries=retry_strategy)
session = requests.Session()
session.trust_env = False
session.mount("http://", adapter)
session.mount("https://", adapter)

Чек-лист по защите системы для инженеров

Источники

← AI Security Directory →