Leadership1 publisher3 min readPublished
MLflow's default server hands out cloud credentials to anyone who asks nicely
A GitHub advisory describes an unauthenticated, full-read SSRF in the default MLflow tracking server that reaches the cloud metadata endpoint and reflects the response back to the caller.
The Board Room · Leadership desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened
- The default MLflow tracking server (mlflow server, no authentication, default SQLite backend) exposes the model-registry webhooks API unauthenticated.
- A synchronous POST /api/2.0/mlflow/webhooks/{id}/test endpoint returns the upstream response status and body to the caller.
- The SSRF guard _validate_webhook_url, added in PR #20747 and shipped in 3.10.0, resolves the webhook hostname and rejects non-public IPs, blocking RFC1918, loopback, link-local and metadata addresses.
- The guard validates but pins nothing: delivery follows HTTP redirects (no allow_redirects=False) and the resolved/validated IP is never carried into the connection.
- 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 the redirect and never re-validates the redirect target.
Compiled by The Board RoomSomething wrong?How this is made
Why it matters
A security advisory on GitHub describes an unauthenticated, full-read server-side request forgery in the default MLflow tracking server, one that reaches cloud metadata endpoints and returns the response straight to the caller [1][6]. The interesting part is not the redirect bug. It is what the default configuration says about how ML tooling was deployed during the last two years: as a lab convenience, not as production infrastructure.
Start with the defaults. Run `mlflow server` and you get no authentication, a SQLite backend, and a model-registry webhooks API that is exposed with no auth in front of it [1]. The only authorization for webhooks lives in an optional auth plugin that is not loaded by default [7]. So the webhook endpoints are reachable by anyone who can reach the server.
The maintainers did try. Version 3.10.0 added an SSRF guard, `_validate_webhook_url`, which resolves the webhook hostname and rejects any non-public IP, blocking loopback, RFC1918, link-local and metadata addresses [3][15]. It works against the obvious attack. In the researcher's negative control, a webhook pointed at `http://127.0.0.1:6379/` was rejected with a 400 [10].
The problem is that the guard validates but pins nothing [4]. Delivery follows HTTP redirects, because the request is not sent with `allow_redirects=False`, and the validated IP is never carried into the connection [4]. The guard re-checks only the original URL, never the redirect target [8]. So an attacker registers a webhook pointing at a public HTTPS host they control, which passes the check, and that host returns a 302 to `http://169.254.169.254/...`; MLflow follows it and never re-validates [5]. Because the synchronous `/test` endpoint reflects the upstream status and body back to the caller, the internal response comes back out [2]. A second path, DNS rebinding, works for the same underlying reason: the guard and the actual connection resolve the name independently with no pinning [9].
In the proof of concept, creating the webhook returned 200 with status ACTIVE, and firing it through the unauthenticated `/test` endpoint returned 200 with the contents of the metadata path in `response_body` [11][12]. The attacker's redirect pointed at `http://169.254.169.254/latest/meta-data/iam/security-credentials/`, the path that vends the IAM role credentials of whatever the server runs as [13]. The researcher confirmed it live against mlflow 3.13.0 on a default SQLite server [14].
Read the chain plainly. No login, one HTTP request to create a webhook, one to fire it, and the server reads its own cloud role credentials aloud [1][11][12]. That is not an exotic exploit. It is the expected outcome when a tool ships with authentication off by default and an experiment tracker ends up holding a cloud identity that can touch storage and model artifacts.
What to watch: whether the fix disables redirect following and pins the validated IP into the connection rather than adding another string check, and whether MLflow moves authentication from an optional plugin to the default posture. For operators, the more useful audit is not this CVE but the inventory question behind it. Find every MLflow, notebook, and vector-store service standing up inside your accounts, and check which of them are exposed unauthenticated while holding an instance role.