CVE-2024-3573 (Python, published 2024-04-16T00:15Z)
Description: value: mlflow/mlflow is vulnerable to Local File Inclusion (LFI) due to improper parsing of URIs, allowing attackers to bypass checks and read arbitrary files on the system. The issue arises from the 'is_local_uri' function's failure to properly handle URIs with empty or 'file' schemes, leading to the misclassification of URIs as non-local. Attackers can exploit this by crafting malicious model versions with specially crafted 'source' parameters, enabling the reading of sensitive files within at least two directory levels from the server's root.
Severity: nan (CVSS v3 base score: )
Weakness: CWE-29 \u2014 Path Traversal: '\..\filename'
The product uses external input to construct a pathname that should be within a restricted directory, but it does not properly neutralize '\..\filename' (leading backslash dot dot) sequences that can resolve to a location that is outside of that directory.
Repository: https://github.com/mlflow/mlflow
Files changed:
mlflow/environment_variables.py
mlflow/server/handlers.py
mlflow/utils/uri.py
tests/artifacts/test_artifacts.py
tests/store/tracking/test_file_store.py
tests/store/tracking/test_sqlalchemy_store.py
tests/tracking/test_rest_tracking.py
tests/utils/test_file_utils.py
tests/utils/test_uri.py
Commit message: Fix local uri (#10651)

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>

Vulnerable code:
#: Specifies whether or not to allow using a file URI as a model version source.
#: Please be aware that setting this environment variable to True is potentially risky
#: because it can allow access to arbitrary files on the specified filesystem
#: (default: ``False``).
MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE = _BooleanEnvironmentVariable(
    "MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE", False
)


from mlflow.environment_variables import (
    MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE,
    MLFLOW_DEPLOYMENTS_TARGET,
)
from mlflow.utils.uri import is_file_uri, is_local_uri, validate_path_is_safe, validate_query_string
    # There might be file URIs that are local but can bypass the above check. To prevent this, we
    # disallow using file URIs as model version sources by default unless it's explicitly allowed
    # by setting the MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE environment variable to True.
    if not MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE.get() and is_file_uri(source):
        raise MlflowException(
            f"Invalid model version source: '{source}'. MLflow tracking server doesn't allow using "
            "a file URI as a model version source for security reasons. To disable this check, set "
            f"the {MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE} environment variable to "
            "True.",
            INVALID_PARAMETER_VALUE,
        )

    if parsed_uri.hostname and not (
    ):
        return False

    return urllib.parse.urlparse(uri).scheme == "file"
from mlflow.utils.file_utils import local_file_uri_to_path, mkdir, path_to_local_file_uri

    experiment_test_2_artifact_location = "file://my_server/my_path/my_sub_path"
    experiment_test_2_id = mlflow.create_experiment(
        "test_exp_e", experiment_test_2_artifact_location
    )
    with mlflow.start_run(experiment_id=experiment_test_2_id) as run:
        with mock.patch("shutil.copy2") as copyfile_mock, mock.patch(
            "os.path.exists", return_value=True
        ) as exists_mock:
            mlflow.log_artifact(text_artifact.artifact_path)
            copyfile_mock.assert_called_once()
            exists_mock.assert_called_once()
            local_path = mlflow.artifacts.download_artifacts(
                run_id=run.info.run_id, artifact_path=text_artifact.artifact_name
            )
            assert (
                local_file_uri_to_path(experiment_test_2_artifact_location)
                + rf"\{run.info.run_id}\artifacts\{text_artifact.artifact_name}"
                == local_path
            )
            "file://my_server/my_path/my_sub_path",
            "file://my_server/my_path/my_sub_path/{e}/{r}/artifacts",
        ("file://my_server/my_path/my_sub_path", "file://my_server/my_path/my_sub_path/{e}"),
        ("file://my_server/my_path/my_sub_path", "file://my_server/my_path/my_sub_path/{e}"),
            "file://my_server/my_path/my_sub_path",
            "file://my_server/my_path/my_sub_path/{e}/{r}/artifacts",
    assert response.status_code == 400
    assert "MLflow tracking server doesn't allow" in response.json()["message"]


def test_create_model_version_with_file_uri_env_var(tmp_path):
    backend_uri = tmp_path.joinpath("file").as_uri()
    with _init_server(
        backend_uri,
        root_artifact_uri=tmp_path.as_uri(),
        extra_env={"MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE": "true"},
    ) as url:
        mlflow_client = MlflowClient(url)

        name = "test"
        mlflow_client.create_registered_model(name)
        exp_id = mlflow_client.create_experiment("test")
        run = mlflow_client.create_run(experiment_id=exp_id)
        response = requests.post(
            f"{mlflow_client.tracking_uri}/api/2.0/mlflow/model-versions/create",
            json={
                "name": name,
                "source": "file://123.456.789.123/path/to/source",
                "run_id": run.info.run_id,
            },
        )
        assert response.status_code == 200
        ("file://my_server/my_path/my_sub_path", r"\\my_server\my_path\my_sub_path"),
    assert not is_local_uri("file://myhostname/path/to/file")
        ("file://myhostname/my/path", "file://myhostname/my/path"),
        ("file://myhostname/my/path", "file://myhostname/my/path"),

Fixed code:
from mlflow.environment_variables import MLFLOW_DEPLOYMENTS_TARGET
from mlflow.utils.uri import is_local_uri, validate_path_is_safe, validate_query_string
    is_remote_hostname = parsed_uri.hostname and not (
    )
        if is_remote_hostname:
            raise MlflowException(
                f"{uri} is not a valid remote uri. For remote access "
                "on windows, please consider using a different scheme "
                "such as SMB (e.g. smb://<hostname>/<path>)."
            )
    if is_remote_hostname:
        return False

    scheme = urllib.parse.urlparse(uri).scheme
    return scheme == "file"
from mlflow.utils.file_utils import mkdir, path_to_local_file_uri
            "\\my_server/my_path/my_sub_path",
            "file:///{drive}my_server/my_path/my_sub_path/{e}/{r}/artifacts",
        ("\\my_server/my_path/my_sub_path", "file:///{drive}my_server/my_path/my_sub_path/{e}"),
        ("\\my_server/my_path/my_sub_path", "file:///{drive}my_server/my_path/my_sub_path/{e}"),
            "\\my_server/my_path/my_sub_path",
            "file:///{drive}my_server/my_path/my_sub_path/{e}/{r}/artifacts",
    assert response.status_code == 500, response.json()
    assert "is not a valid remote uri" in response.json()["message"]
    with pytest.raises(MlflowException, match="is not a valid remote uri."):
        is_local_uri("file://myhostname/path/to/file")

        ("file://localhost/my/path", "file://localhost/my/path"),
        ("\\myhostname/my/path", "file:///{drive}myhostname/my/path"),