CVE-2023-2780 (Python, published 2023-05-17T21:15Z)
Description: value: Path Traversal: '\..\filename' in GitHub repository mlflow/mlflow prior to 2.3.1.
Severity: nan (CVSS v3 base score: 9.8)
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/tracking/integration_test_utils.py
tests/tracking/test_rest_tracking.py
tests/utils/test_uri.py
Commit message: Disallow using a file URI as model version source (#8126)

* Add a test

Signed-off-by: harupy <17039389+harupy@users.noreply.github.com>

* Add a test

Signed-off-by: harupy <17039389+harupy@users.noreply.github.com>

* Disallow file URI

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Add a test

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Ensure artifact URI is a file URI

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Add a new test

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Fix doc comment

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Add test case

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Fix is_local_uri

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Fix valiation logic

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Include 127.0.0.1

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Update comment

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Remove dot

Signed-off-by: harupy <hkawamura0130@gmail.com>

* Update mlflow/environment_variables.py

Signed-off-by: Harutaka Kawamura <hkawamura0130@gmail.com>

---------

Signed-off-by: harupy <17039389+harupy@users.noreply.github.com>
Signed-off-by: harupy <hkawamura0130@gmail.com>
Signed-off-by: Harutaka Kawamura <hkawamura0130@gmail.com>

Vulnerable code:
from mlflow.utils.uri import is_local_uri
    if not is_local_uri(source):
        return

    if run_id:
        store = _get_tracking_store()
        run = store.get_run(run_id)
        source = pathlib.Path(local_file_uri_to_path(source)).resolve()
        run_artifact_dir = pathlib.Path(local_file_uri_to_path(run.info.artifact_uri)).resolve()
        if run_artifact_dir in [source, *source.parents]:
            return

    raise MlflowException(
        f"Invalid source: '{source}'. To use a local path as source, the run_id request parameter "
        "has to be specified and the local path has to be contained within the artifact directory "
        "of the run specified by the run_id.",
        INVALID_PARAMETER_VALUE,
    )
    if parsed_uri.hostname:
def _init_server(backend_uri, root_artifact_uri):
def test_create_model_version_with_local_source(mlflow_client):
            "source": run.info.artifact_uri[len("file://") :],
            "run_id": run.info.run_id,
    assert response.status_code == 200
            "source": run.info.artifact_uri,
    resp = response.json()
    assert "Invalid source" in resp["message"]
            "source": "/tmp",
    resp = response.json()
    assert "Invalid source" in resp["message"]

Fixed 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.utils.uri import is_local_uri, is_file_uri
from mlflow.environment_variables import MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE
    if is_local_uri(source):
        if run_id:
            store = _get_tracking_store()
            run = store.get_run(run_id)
            source = pathlib.Path(local_file_uri_to_path(source)).resolve()
            run_artifact_dir = pathlib.Path(local_file_uri_to_path(run.info.artifact_uri)).resolve()
            if run_artifact_dir in [source, *source.parents]:
                return

        raise MlflowException(
            f"Invalid model version source: '{source}'. To use a local path as a model version "
            "source, the run_id request parameter has to be specified and the local path has to be "
            "contained within the artifact directory of the run specified by the run_id.",
            INVALID_PARAMETER_VALUE,
        )

    # 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.name} environment variable to "
            "True.",
            INVALID_PARAMETER_VALUE,
        )
    if parsed_uri.hostname and not (
        parsed_uri.hostname == "."
        or parsed_uri.hostname.startswith("localhost")
        or parsed_uri.hostname.startswith("127.0.0.1")
    ):
def is_file_uri(uri):
    return urllib.parse.urlparse(uri).scheme == "file"


def _init_server(backend_uri, root_artifact_uri, extra_env=None):
            **(extra_env or {}),
def test_create_model_version_with_path_source(mlflow_client):
    response = requests.post(
        f"{mlflow_client.tracking_uri}/api/2.0/mlflow/model-versions/create",
        json={
            "name": name,
            "source": run.info.artifact_uri[len("file://") :],
            "run_id": run.info.run_id,
        },
    )
    assert response.status_code == 200

    # run_id is not specified
    response = requests.post(
        f"{mlflow_client.tracking_uri}/api/2.0/mlflow/model-versions/create",
        json={
            "name": name,
            "source": run.info.artifact_uri[len("file://") :],
        },
    )
    assert response.status_code == 400
    assert "To use a local path as a model version" in response.json()["message"]

    # run_id is specified but source is not in the run's artifact directory
    response = requests.post(
        f"{mlflow_client.tracking_uri}/api/2.0/mlflow/model-versions/create",
        json={
            "name": name,
            "source": "/tmp",
            "run_id": run.info.run_id,
        },
    )
    assert response.status_code == 400
    assert "To use a local path as a model version" in response.json()["message"]


def test_create_model_version_with_file_uri(mlflow_client):
    name = "test"
    mlflow_client.create_registered_model(name)
    exp_id = mlflow_client.create_experiment("test")
    run = mlflow_client.create_run(experiment_id=exp_id)
    assert run.info.artifact_uri.startswith("file://")
    # run_id is not specified
            "source": run.info.artifact_uri,
    assert response.status_code == 400
    assert "To use a local path as a model version" in response.json()["message"]
    # run_id is specified but source is not in the run's artifact directory
            "source": "file:///tmp",
    assert "To use a local path as a model version" in response.json()["message"]
            "source": "file://123.456.789.123/path/to/source",
    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()
    url, process = _init_server(
        backend_uri,
        root_artifact_uri=tmp_path.as_uri(),
        extra_env={"MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE": "true"},
    )
    try:
        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
    finally:
        _terminate_server(process)
    assert is_local_uri("file://./mlruns")
    assert is_local_uri("file://localhost/mlruns")
    assert is_local_uri("file://localhost:5000/mlruns")
    assert is_local_uri("file://127.0.0.1/mlruns")
    assert is_local_uri("file://127.0.0.1:5000/mlruns")