FlawOpen

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

● CVE-2026-64849 · CVSS 8.6 · 높음
보안 연구 · FlawOpen

CVE-2026-64849: MLflow Webhook SSRF 취약점

CVE-2026-64849는 MLflow webhook 전달 기능의 인증 없는 서버 측 요청 위조(SSRF)입니다 (High, CVSS 3.1: 8.6). 최초 URL 검사는 리디렉션 대상이나 실제 연결 주소를 제한하지 않았고, 테스트 endpoint가 상위 응답 본문을 호출자에게 반환할 수 있었습니다.[1]

💡 알기 쉬운 설명 (ELI5)

접수 담당자가 소포 주소를 확인했지만 전달 메모의 새 주소는 확인하지 않고 배송원이 따르게 하는 상황을 떠올려 보세요. MLflow는 연결 전에 webhook URL을 확인했지만 리디렉션과 두 번째 DNS 조회가 요청을 다른 곳으로 보낼 수 있었습니다. 수정은 HTTP 데이터를 보내기 전에 실제 연결된 peer 주소를 확인합니다.[1], [2]

핵심 개념 및 용어

SSRF
호출자가 선택한 네트워크 요청을 서버가 대신 실행해 서버의 네트워크 신뢰 경계를 넘는 문제.[1]
Webhook 테스트 endpoint
설정된 webhook으로 테스트 이벤트를 보내는 MLflow API. 영향을 받는 기본 서버는 상위 응답 본문을 호출자에게 반환했습니다.[1]
DNS rebinding / TOCTOU
검증 시 공인 IP로 해석된 hostname이 연결 시 다른 비공인 IP로 해석되는 상황.[1], [2]

프레임워크 매핑

단계별 공격 실행 흐름

단계 1

접근 가능한 MLflow 서버가 webhook 테스트를 받음

권고문은 인증 없는 기본 Tracking Server 구성을 설명합니다. 인증 플러그인과 배포 제어에 따라 접근성이 달라지므로 실제 구성을 확인하세요. CISA는 2026년 8월 19일 이 CVE를 Known Exploited Vulnerabilities 카탈로그에 추가했습니다. 알려진 악용을 뜻하지만 특정 배포 환경의 침해를 입증하지는 않습니다.[1], [4], [5]

단계 2

최초 URL이 검증을 통과함

_validate_webhook_url은 원래 hostname을 조회해 확인하지만, 확인된 주소를 이후 socket에 고정하지 않고 버립니다.[1]

단계 3

리디렉션 또는 재조회가 다른 대상에 연결함

HTTP 클라이언트는 리디렉션을 따르고 연결 시 hostname을 다시 조회할 수 있습니다. 테스트 endpoint가 상위 본문을 반환해 전체 응답을 읽는 SSRF가 됩니다.[1]

단계 4

상위 수정은 연결된 peer를 확인함

PR #24258은 연결 직후 TLS/HTTP 데이터 전 socket peer를 검증하고 환경 proxy를 비활성화합니다. CVE는 MLflow 3.15.0에서 수정됐고 별도 IPv6 전환 우회는 3.16.0에서 수정됐습니다. 권고문은 최초 비공개 제보자로 freeman-bb, 독립 발견자로 AUTHENSOR를 명시합니다.[1], [2], [3], [6]

소스 코드 비교: 취약한 구현 vs 보안 패치

코드 조각은 URL 검증과 요청 사이의 경계 및 연결 시 peer 확인을 보여줍니다. 공격 요청과 내부 대상은 포함하지 않습니다. 3.16.0의 IPv6 정규화는 이후 별도로 추가된 강화입니다.[2]

취약한 구현
# PR #24258 이전 mlflow/webhooks/delivery.py 발췌입니다.
_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 →