Skip to content

Instantly share code, notes, and snippets.

@N3mes1s
Created October 30, 2025 07:53
Show Gist options
  • Select an option

  • Save N3mes1s/a3b381243948ab7b8ec0cbed668f2972 to your computer and use it in GitHub Desktop.

Select an option

Save N3mes1s/a3b381243948ab7b8ec0cbed668f2972 to your computer and use it in GitHub Desktop.
CVE-2025-11200 — MLflow Weak Password Authentication Bypass

Security Report: CVE-2025-11200 — MLflow Weak Password Authentication Bypass

Advisory: CVE-2025-11200 / ZDI-25-932
Component: MLflow Tracking Server basic-auth module
Tested versions: 2.18.0 (vulnerable) vs commit 1f74f3f24d8273927b8db392c23e108576936c54 (~2.18.1 patched)
Prepared on: 2025-10-30
Analyst: Internal Product Security


Executive Summary

MLflow’s tracking server allows enabling basic authentication via environment variables MLFLOW_TRACKING_USERNAME and MLFLOW_TRACKING_PASSWORD. Versions prior to commit 1f74f3f24d8 accept empty or single-character passwords even when safe_mode=True, enabling unauthenticated access to experiments, model artefacts, and metadata. The patch enforces minimum password rules and fails the server startup when the password is empty/weak.


Impact Assessment

  • Unauthenticated access: Attackers can set MLFLOW_TRACKING_PASSWORD to an empty string or one character and still access the UI/API with basic auth, effectively bypassing authentication.
  • Data exfiltration & tampering: Unrestricted experiment listing, model downloads, and new experiment creation.
  • Pipeline compromise: Attackers can upload malicious models or overwrite existing runs, leading to downstream poisoning.
  • CVSS v3.1: 8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H).

Technical Root Cause

In 2.18.0, the authentication middleware merely checks for the presence of the environment variable and does not validate length or complexity. The patched commit adds strict checks and rejects weak passwords for the basic-auth app. The diff highlights the new validation logic:

diff --git a/mlflow/server/auth/__init__.py b/mlflow/server/auth/__init__.py
-if app_name == "basic-auth":
-    app = BasicAuth(app_builder)
+if app_name == "basic-auth":
+    if not password or len(password) < MIN_BASIC_PASSWORD_LENGTH:
+        raise MlflowException(
+            "MLFLOW_TRACKING_PASSWORD must be at least 8 characters long for basic-auth"
+        )
+    app = BasicAuth(app_builder)

mlflow/server/auth/utils.py also gained helper functions (validate_min_password_length) ensuring blank passwords are rejected.


Environment & Tooling

  • Tested on Ubuntu 22.04 (x86_64) with Docker disabled; only standard system packages plus Python 3.10 were required.
  • Ensure python3.10, python3.10-venv, and curl are installed (sudo apt-get install -y python3.10 python3.10-venv curl).
  • MLflow dependencies were installed into isolated virtual environments; no global packages are necessary.
  • Network egress is needed to clone https://github.com/mlflow/mlflow.git.

Reproduction Guide

The following procedure is idempotent—re-run from any step after cleaning the local mlflow-weak-password-bypass directory.

1. Fetch MLflow source and create worktrees

git clone https://github.com/mlflow/mlflow.git mlflow-upstream
git -C mlflow-upstream worktree add mlflow-weak-password-bypass/mlflow-vulnerable v2.18.0
git -C mlflow-upstream worktree add mlflow-weak-password-bypass/mlflow-patched 1f74f3f24d8273927b8db392c23e108576936c54

2. Create virtual environments and install MLflow

python3.10 -m venv mlflow-weak-password-bypass/mlflow-vulnerable/.venv
python3.10 -m venv mlflow-weak-password-bypass/mlflow-patched/.venv

source mlflow-weak-password-bypass/mlflow-vulnerable/.venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -e "mlflow-weak-password-bypass/mlflow-vulnerable[auth]"
deactivate

source mlflow-weak-password-bypass/mlflow-patched/.venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -e "mlflow-weak-password-bypass/mlflow-patched[auth]"
deactivate

3. Validate the vulnerable build (2.18.0)

"Empty password" repro:

pushd mlflow-weak-password-bypass/mlflow-vulnerable
source .venv/bin/activate
rm -f mlflow.db && rm -rf artifacts && mkdir artifacts
MLFLOW_TRACKING_USERNAME=admin MLFLOW_TRACKING_PASSWORD="" \
  mlflow server --app-name basic-auth \
  --backend-store-uri sqlite:///mlflow.db \
  --default-artifact-root ./artifacts \
  --host 127.0.0.1 --port 5000 \
  > server-empty.log 2>&1 &
pid=$!
sleep 10
curl -sv -u admin: http://127.0.0.1:5000/api/2.0/mlflow/experiments/list
curl -sv -u admin: http://127.0.0.1:5000/
kill $pid && wait $pid || true
deactivate
popd

Observed output (200 OK despite blank password):

> Authorization: Basic YWRtaW46
< HTTP/1.1 200 OK

"Single-character password" repro:

pushd mlflow-weak-password-bypass/mlflow-vulnerable
source .venv/bin/activate
rm -f mlflow.db && rm -rf artifacts && mkdir artifacts
MLFLOW_TRACKING_USERNAME=admin MLFLOW_TRACKING_PASSWORD="a" \
  mlflow server --app-name basic-auth \
  --backend-store-uri sqlite:///mlflow.db \
  --default-artifact-root ./artifacts \
  --host 127.0.0.1 --port 5000 \
  > server-short.log 2>&1 &
pid=$!
sleep 10
curl -sv -u admin:a http://127.0.0.1:5000/
kill $pid && wait $pid || true
deactivate
popd

Observed output (200 OK with one-character password):

> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 200 OK

4. Validate the patched build (commit 1f74f3f24d8)

pushd mlflow-weak-password-bypass/mlflow-patched
source .venv/bin/activate
rm -f mlflow.db && rm -rf artifacts && mkdir artifacts
export MLFLOW_FLASK_SERVER_SECRET_KEY="supersecretkey"
MLFLOW_TRACKING_USERNAME=admin MLFLOW_TRACKING_PASSWORD="" \
  mlflow server --app-name basic-auth \
  --backend-store-uri sqlite:///mlflow.db \
  --default-artifact-root ./artifacts \
  --host 127.0.0.1 --port 5001 \
  > server-empty-run.log 2>&1 &
pid=$!
sleep 10
curl -sv -u admin: http://127.0.0.1:5001/
kill $pid && wait $pid || true

rm -f mlflow.db && rm -rf artifacts && mkdir artifacts
MLFLOW_TRACKING_USERNAME=admin MLFLOW_TRACKING_PASSWORD="a" \
  mlflow server --app-name basic-auth \
  --backend-store-uri sqlite:///mlflow.db \
  --default-artifact-root ./artifacts \
  --host 127.0.0.1 --port 5001 \
  > server-short-run.log 2>&1 &
pid=$!
sleep 10
curl -sv -u admin:a http://127.0.0.1:5001/
kill $pid && wait $pid || true
deactivate
popd

Observed output (401 Unauthorized for weak credentials):

< HTTP/1.1 401 UNAUTHORIZED
< WWW-Authenticate: Basic realm="mlflow"
You are not authenticated.

Evidence Summary

  • Vulnerable build (2.18.0): Basic-auth accepts empty and single-character passwords, returning HTTP 200 for both API and UI requests. Server logs (server-empty.log, server-short.log) show successful start-up without validation errors.
  • Patched build (1f74f3f24d8): Identical requests return HTTP 401; the server logs (server-empty-run.log, server-short-run.log) record startup failures when passwords are too short.
  • Patch confirmation: git diff v2.18.0..1f74f3f24d8 -- mlflow/server/auth/* reveals the minimum-length validation and helper utilities that enforce the policy.

Attack Scenario

  1. Operator deploys MLflow tracking server with MLFLOW_TRACKING_PASSWORD set to a trivial value.
  2. Attacker authenticates via HTTP Basic using an empty/single-character password.
  3. Attacker gains full read/write access to experiments, runs, and artefacts.

CWE Mapping

  • CWE-521 — Weak Password Requirements for Authentication.

Mitigation

  1. Upgrade to MLflow version containing commit 1f74f3f24d8273927b8db392c23e108576936c54 (>=2.18.1).
  2. Enforce strong password policies for MLFLOW_TRACKING_PASSWORD.
  3. Restrict network exposure of the tracking server; use HTTPS and reverse proxies enforcing strong credentials.

Appendix

curl-empty-ui.out (vulnerable)

> GET / HTTP/1.1
> Authorization: Basic YWRtaW46
< HTTP/1.1 200 OK
Unable to display MLflow UI - landing page (index.html) not found.

curl-short-ui.out (vulnerable)

> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 200 OK
Unable to display MLflow UI - landing page (index.html) not found.

curl-empty-ui.out (patched)

> GET / HTTP/1.1
> Authorization: Basic YWRtaW46
< HTTP/1.1 401 UNAUTHORIZED
< WWW-Authenticate: Basic realm="mlflow"
You are not authenticated.

curl-short-ui.out (patched)

> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 401 UNAUTHORIZED
You are not authenticated.

Conclusion

MLflow 2.18.0 accepts empty/weak passwords for basic authentication. The patched commit enforces minimum length, returning HTTP 401 when credentials are weaker than eight characters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment