VYPR
Medium severity4.3NVD Advisory· Published Apr 7, 2026· Updated Apr 20, 2026

CVE-2026-33866

CVE-2026-33866

Description

MLflow is vulnerable to an authorization bypass affecting the AJAX endpoint used to download saved model artifacts. Due to missing access‑control validation, a user without permissions to a given experiment can directly query this endpoint and retrieve model artifacts they are not authorized to access.

This issue affects MLflow version through 3.10.1

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mlflowPyPI
<= 3.10.1

Affected products

1

Patches

1
005b959cacda

Enforce auth on logged model artifact download AJAX endpoint (#21708)

https://github.com/mlflow/mlflowYuki WatanabeMar 23, 2026via ghsa
2 files changed · +95 1
  • mlflow/server/auth/__init__.py+11 0 modified
    @@ -121,6 +121,7 @@
         GetWorkspace,
         ListArtifacts,
         ListGatewayEndpointBindings,
    +    ListLoggedModelArtifacts,
         ListScorers,
         ListScorerVersions,
         ListWorkspaces,
    @@ -237,6 +238,7 @@
     from mlflow.server.fastapi_app import create_fastapi_app
     from mlflow.server.handlers import (
         _disable_if_workspaces_disabled,
    +    _get_ajax_path,
         _get_model_registry_store,
         _get_request_message,
         _get_tracking_store,
    @@ -1652,6 +1654,7 @@ def _re_compile_path(path: str) -> re.Pattern:
         FinalizeLoggedModel: validate_can_update_logged_model,
         DeleteLoggedModelTag: validate_can_delete_logged_model,
         SetLoggedModelTags: validate_can_update_logged_model,
    +    ListLoggedModelArtifacts: validate_can_read_logged_model,
         LogLoggedModelParamsRequest: validate_can_update_logged_model,
     }
     
    @@ -1666,6 +1669,14 @@ def get_logged_model_before_request_handler(request_class):
         for http_path, handler, methods in get_endpoints(get_logged_model_before_request_handler)
         for method in methods
     }
    +# The AJAX artifact download endpoint is a plain Flask route with a path parameter, so it
    +# can't go in routes.py/BEFORE_REQUEST_VALIDATORS (exact match) and must be added here.
    +LOGGED_MODEL_BEFORE_REQUEST_VALIDATORS[
    +    (
    +        _re_compile_path(_get_ajax_path("/mlflow/logged-models/<model_id>/artifacts/files")),
    +        "GET",
    +    )
    +] = validate_can_read_logged_model
     
     WEBHOOK_BEFORE_REQUEST_HANDLERS = {
         CreateWebhook: sender_is_admin,
    
  • tests/server/auth/test_auth.py+84 1 modified
    @@ -32,14 +32,15 @@
         ErrorCode,
     )
     from mlflow.server import auth as auth_module
    -from mlflow.server.auth import _authenticate_fastapi_request
    +from mlflow.server.auth import _authenticate_fastapi_request, _re_compile_path
     from mlflow.server.auth.routes import (
         AJAX_LIST_USERS,
         CREATE_REGISTERED_MODEL_PERMISSION,
         GET_REGISTERED_MODEL_PERMISSION,
         GET_SCORER_PERMISSION,
         LIST_USERS,
     )
    +from mlflow.server.handlers import STATIC_PREFIX_ENV_VAR, _get_ajax_path
     from mlflow.utils.os import is_windows
     from mlflow.utils.workspace_utils import DEFAULT_WORKSPACE_NAME
     
    @@ -783,6 +784,88 @@ def predict(self, context, model_input):
                 client.delete_logged_model(model_id=model.model_id)
     
     
    +@pytest.mark.parametrize(
    +    "client",
    +    [{"MLFLOW_AUTH_CONFIG_PATH": "tests/server/auth/fixtures/no_permission_auth.ini"}],
    +    indirect=True,
    +)
    +def test_logged_model_artifact_authorization(client: MlflowClient, monkeypatch: pytest.MonkeyPatch):
    +    username1, password1 = create_user(client.tracking_uri)
    +    username2, password2 = create_user(client.tracking_uri)
    +
    +    with User(username1, password1, monkeypatch):
    +        exp_id = client.create_experiment("logged-model-artifact-authz-test")
    +        model = client.create_logged_model(experiment_id=exp_id)
    +
    +    # user1 (owner) should be able to access the artifact endpoint (404 since no artifact
    +    # exists, but should NOT be 403)
    +    response = requests.get(
    +        url=(
    +            client.tracking_uri
    +            + f"/ajax-api/2.0/mlflow/logged-models/{model.model_id}/artifacts/files"
    +        ),
    +        params={"artifact_file_path": "test.txt"},
    +        auth=(username1, password1),
    +    )
    +    assert response.status_code != 403
    +
    +    # user2 has no permission on the experiment — expect 403
    +    response = requests.get(
    +        url=(
    +            client.tracking_uri
    +            + f"/ajax-api/2.0/mlflow/logged-models/{model.model_id}/artifacts/files"
    +        ),
    +        params={"artifact_file_path": "test.txt"},
    +        auth=(username2, password2),
    +    )
    +    assert response.status_code == 403
    +
    +    # Also verify the list-artifacts (directories) endpoint
    +    # user1 (owner) should be able to list artifacts
    +    response = requests.get(
    +        url=(
    +            client.tracking_uri
    +            + f"/api/2.0/mlflow/logged-models/{model.model_id}/artifacts/directories"
    +        ),
    +        auth=(username1, password1),
    +    )
    +    assert response.status_code != 403
    +
    +    # user2 has no permission — expect 403
    +    response = requests.get(
    +        url=(
    +            client.tracking_uri
    +            + f"/api/2.0/mlflow/logged-models/{model.model_id}/artifacts/directories"
    +        ),
    +        auth=(username2, password2),
    +    )
    +    assert response.status_code == 403
    +
    +
    +def test_logged_model_artifact_validator_respects_static_prefix(
    +    monkeypatch: pytest.MonkeyPatch,
    +):
    +    base = "/mlflow/logged-models/<model_id>/artifacts/files"
    +
    +    # Without prefix — should match the bare path
    +    pat_no_prefix = _re_compile_path(_get_ajax_path(base))
    +    assert pat_no_prefix.fullmatch("/ajax-api/2.0/mlflow/logged-models/abc123/artifacts/files")
    +
    +    # With prefix — should match the prefixed path
    +    monkeypatch.setenv(STATIC_PREFIX_ENV_VAR, "/custom-prefix")
    +    _re_compile_path.cache_clear()
    +    pat_with_prefix = _re_compile_path(_get_ajax_path(base))
    +    assert pat_with_prefix.fullmatch(
    +        "/custom-prefix/ajax-api/2.0/mlflow/logged-models/abc123/artifacts/files"
    +    )
    +    # bare path should NOT match the prefixed pattern
    +    assert not pat_with_prefix.fullmatch(
    +        "/ajax-api/2.0/mlflow/logged-models/abc123/artifacts/files"
    +    )
    +
    +    _re_compile_path.cache_clear()
    +
    +
     def test_search_logged_models(client: MlflowClient, monkeypatch: pytest.MonkeyPatch):
         username1, password1 = create_user(client.tracking_uri)
         username2, password2 = create_user(client.tracking_uri)
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.