MLflow CVE-2026-64849: an Unauthenticated SSRF That Steals Your Cloud Keys
MLflow's unauthenticated SSRF (CVE-2026-64849, CVSS 9.3) was exploited within hours to reach cloud metadata endpoints and lift IAM credentials. What broke, and the fix runbook.
The tracking server your data scientists left on a public IP is now a proxy into your cloud account. CVE-2026-64849 is an unauthenticated, full-read server-side request forgery in MLflow — the most widely deployed experiment-tracking and model-registry platform in the machine-learning stack — and attackers started scanning for it within hours of the identifier being assigned on 17 August 2026. It carries a CVSS of 9.3, it needs no credentials, and the thing it reads back is whatever lives on your internal network: most usefully, the cloud instance metadata endpoint that hands out short-lived IAM keys to anyone who can make the host ask for them.
This is not a theoretical chain. The security firm watchTowr said its Attacker Eye honeypot network caught bad actors "indiscriminately scanning for exposed MLflow instances online" the same day the CVE was published, with the explicit goal of extracting "credentials and secrets from well-known internal IP addresses and services," per The Hacker News reporting. VulnCheck confirmed the activity independently. CISA agreed fast enough to add the bug to its Known Exploited Vulnerabilities catalog on 19 August, with a federal remediation deadline of 2 September.
What CVE-2026-64849 actually is
MLflow's model registry can fire webhooks when events happen — a model version gets promoted, a tag changes. To let operators sanity-check a webhook, the server exposes a test endpoint: POST /api/2.0/mlflow/webhooks/{id}/test. On a default install that endpoint is unauthenticated, because MLflow ships with no authentication at all and expects you to bolt it on.
The server does try to be careful. Before it delivers the request, _validate_webhook_url() in mlflow/utils/validation.py checks that the target URL doesn't resolve to a private, loopback, or link-local address — the standard SSRF guard. The problem, described in the GitHub Security Advisory GHSA-7gwp-5pfp-969j, is that the check and the connection are two separate acts, and nothing is carried between them:
- The validator pins nothing. It resolves the hostname, decides the resolved IP looks public, and then throws that IP away. The delivery code in
mlflow/webhooks/delivery.pyre-resolves the name and connects fresh. - Redirects are never re-validated. Point the webhook at an attacker-controlled HTTPS URL that passes the check, then answer with a
302tohttp://169.254.169.254/…. The HTTP client follows it, and — because this is the test endpoint — reflects the response body straight back to the caller. Full read. - DNS rebinding closes the gap the redirect can't. Because
getaddrinfo()runs twice, an attacker who controls a DNS record can answer with a public IP at validation time and a link-local IP microseconds later at connection time. Classic time-of-check/time-of-use.
Two independent bypasses, one root cause: the guard validated a string, not a socket.
Why an SSRF in MLflow reaches your cloud keys
SSRF is only as dangerous as what the vulnerable host can reach that you can't. On a laptop, an SSRF is a curiosity. On a cloud instance, it is a credential dispenser, because every major cloud runs a link-local metadata service the workload uses to fetch its own identity:
| Cloud | Metadata endpoint | What an SSRF reads |
|---|---|---|
| AWS (IMDSv1) | http://169.254.169.254/latest/meta-data/iam/security-credentials/ |
Temporary IAM role keys, no header required |
| GCP | http://169.254.169.254/computeMetadata/v1/ |
Service-account tokens (needs Metadata-Flavor header) |
| Azure | http://169.254.169.254/metadata/identity/oauth2/token |
Managed-identity access tokens (needs Metadata:true header) |
The AWS IMDSv1 row is the money shot: a single unauthenticated GET, no custom header, and the response is a valid set of temporary keys for whatever IAM role the MLflow instance runs as. In an ML environment that role is rarely minimal — it usually reads and writes the S3 buckets holding training data, model artifacts, and sometimes the whole feature store. The attacker doesn't need to break into your cloud. Your MLflow server logs in for them and reads the answer aloud.
"We validated the URL you gave us." "You did. Then you followed my redirect to the metadata service and read the IAM keys back to me in the test response. You validated a promise, not a destination."
That exchange is the entire bug. Everything after it — enumerating S3, assuming roles, pivoting into the account — is ordinary cloud tradecraft against valid credentials, which is exactly why it evades the controls trained to spot invalid ones.
The patch that a redirect walked around
This is the part worth sitting with, because it is a pattern, not an accident. MLflow had already hardened webhook URL validation in a prior release. watchTowr's Yordan Ganchev noted the new bug "bypasses prior fixes because of how it handles web redirects." The earlier fix reasoned about the URL the operator typed. It never reasoned about the URL the HTTP client would end up talking to after a 302 or a rebind.
Every SSRF allowlist that validates a hostname instead of the connected socket has this hole. The correct fix — shipped in MLflow 3.15.0 — is to validate at the network layer: the release introduces an SSRFProtectedHTTPAdapter that checks the peer IP of each socket immediately after connect(), so redirect targets and rebound resolutions are re-checked at the only moment that can't be spoofed. That single design change closes the 302-read variant, the 307/308-write variant, and the DNS-rebinding TOCTOU together. Validating strings is guessing; validating the connected peer is knowing.
Your AI stack is now the soft target
MLflow is one data point in a much larger shift. Unit 42's "Frontier AI Vulnerability Burst", published 4 August 2026, describes an autonomous system that surfaced 14,090 confirmed vulnerabilities across 3,915 open-source projects, roughly 40% of them high or critical severity. A large share of the ML/AI tooling ecosystem — trackers, registries, serving frameworks, agent orchestrators — was written for a trusted lab network and is now sitting on the perimeter with default-open endpoints. The attack surface that used to belong to web apps and VPN appliances now includes the plumbing of your model pipeline.
The uncomfortable properties of that plumbing:
- It runs with generous cloud roles, because training reads and writes a lot of storage.
- It ships auth-off by default, MLflow being the textbook case.
- It lives near your crown jewels — the data and models — while being watched like a dev tool, not a production edge service.
- It is discovered by machines now, not people, so the window between "shipped" and "scanned" is hours, as CVE-2026-64849 demonstrated in real time.
Remediation
Treat any internet-reachable MLflow instance running below 3.15.0 as credential-exposed until proven otherwise, not merely vulnerable. The metadata read is fast and leaves little on the MLflow box itself.
1. Am I affected?
Check the version and the exposure:
# Version — anything < 3.15.0 is vulnerable
python -c "import mlflow; print(mlflow.__version__)"
mlflow --version
# Is the tracking server reachable, and is the test endpoint open?
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST http://<mlflow-host>:5000/api/2.0/mlflow/webhooks/0/test
# A 400/404 (not 401/403) means the endpoint answers unauthenticated — you are exposed.
If the host runs in AWS/GCP/Azure with an attached role or managed identity, and it was reachable from the internet before you patched, proceed to step 4 regardless of what the logs show.
2. Patch — exact fixed version
Upgrade to MLflow 3.15.0 or later (GHSA-7gwp-5pfp-969j). This is the only complete fix; it adds connection-time peer-IP validation that survives redirects and rebinding.
pip install --upgrade "mlflow>=3.15.0"
3. Can't patch now? Compensating controls
Stack these; none alone is sufficient.
- Enforce IMDSv2 with hop limit 1 (AWS):
aws ec2 modify-instance-metadata-options --http-tokens required --http-put-response-hop-limit 1 --http-endpoint enabled. The token requirement defeats the simpleGET; hop-limit-1 stops container escapes. On GCP/Azure the equivalent is the required metadata header, which a plain SSRF cannot add. - Egress-filter the MLflow host so it cannot originate connections to
169.254.169.254,fd00:ec2::254, loopback, or RFC1918 ranges it has no reason to reach. - Put authentication in front of MLflow — reverse proxy with authn, or MLflow's
--app-name basic-auth. It has none by default. - Take it off the internet. A tracking server has no business on a public IP; bind it to the VPC and reach it over a bastion or private link.
4. Hunt for compromise
Signals, mapped to MITRE ATT&CK:
- Outbound requests from the MLflow host to
169.254.169.254/fd00:ec2::254— the metadata read (T1552.005, Unsecured Credentials: Cloud Instance Metadata API). POST /api/2.0/mlflow/webhooks/{id}/testfrom external or unauthenticated sources, especially where the webhook URL resolves to a private or link-local address (T1190, Exploit Public-Facing Application).- Use of the instance role from an IP that isn't the instance — in CloudTrail, the role's temporary keys called from an external ASN (T1078.004, Valid Accounts: Cloud Accounts).
- Anomalous egress from a host that historically only ingests — a training node suddenly making sustained outbound web requests.
- Access to the model registry / experiment store you didn't initiate (T1213, Data from Information Repositories).
5. Eradicate and verify
- Rotate every credential the host could reach — the instance-role session, any long-lived keys in environment or config, registry and database credentials, and secrets stored in MLflow webhooks. If IMDSv1 was enabled, assume the role keys leaked.
- Review CloudTrail / cloud audit logs for use of the exposed role from outside your infrastructure, and for any
sts:AssumeRole,s3:GetObject, orsecretsmanager:GetSecretValuecalls you can't account for. - Confirm clean after patching, not before: rotating keys while the SSRF is still open just hands the attacker the new ones. Patch, then rotate, then verify no residual access with the old identity.
Where Zero Hunt fits
The dangerous half of this bug isn't the SSRF — it's the seconds after the SSRF, when a valid IAM session leaves your account through the front door. Signature and identity controls wave it through, because from the cloud's point of view your MLflow instance did nothing it isn't allowed to do. What gives it away is the traffic: a tracking host that normally only pulls artifacts suddenly opening a session to the link-local metadata address, then a burst of egress to a never-before-seen destination as the stolen keys get used.
That is the exact signature Zero Hunt's AI Traffic Analysis engine was built to catch. A proprietary deep-learning model with four parallel inference heads — suspicious traffic, malware classification, attack-type identification, application fingerprinting — trained on billions of PCAP sequences and running locally on the appliance GPU at 2.7+ Gbit/s, flags the metadata-endpoint reach and the anomalous outbound session as they happen, not in the next morning's SIEM digest. It sees the SSRF-to-credential pivot as a behavioural anomaly regardless of whether the CVE has a signature yet.
Ahead of that, Zero Hunt's 10-agent generative pentest treats a new MLflow instance on the perimeter as a change-triggered campaign: the engine writes a per-target exploit that chains the redirect and DNS-rebinding bypass, proves whether your IMDS is reachable, and — because the finding is ECDSA-signed with its full evidence chain — hands you a defensible record of exactly which cloud role was exposed, so the rotation in step 5 targets the right identity instead of every key you own. Validate the exposure before an attacker does; watch the wire for the moment they try anyway.