CVE-2024-3573: Path Traversal: '\..\filename' (CWE-29)
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.
CWE description: 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.
Published: 2024-04-16T00:15Z
Repository: https://github.com/mlflow/mlflow
Language: Python
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 date: 2024-01-11 09:50:14 +0800
Commit message: Fix local uri (#10651)

Signed-off-by: Serena Ruan <serena.rxy@gmail.com>
Diff: diff --git a/mlflow/environment_variables.py b/mlflow/environment_variables.py
index e5d9f9cc40..5267bcfe36 100644
--- a/mlflow/environment_variables.py
+++ b/mlflow/environment_variables.py
@@ -264,19 +264,10 @@ MLFLOW_HUGGINGFACE_USE_LOW_CPU_MEM_USAGE = _BooleanEnvironmentVariable(
 #: This can be set to override the 500MB default.
 MLFLOW_HUGGINGFACE_MODEL_MAX_SHARD_SIZE = _EnvironmentVariable(
     "MLFLOW_HUGGINGFACE_MODEL_MAX_SHARD_SIZE", str, "500MB"
 )
 
-#: 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
-)
-
-
 #: Specifies the name of the Databricks secret scope to use for storing OpenAI API keys.
 MLFLOW_OPENAI_SECRET_SCOPE = _EnvironmentVariable("MLFLOW_OPENAI_SECRET_SCOPE", str, None)
 
 #: Specifier whether or not to retry OpenAI API calls.
 MLFLOW_OPENAI_RETRIES_ENABLED = _BooleanEnvironmentVariable("MLFLOW_OPENAI_RETRIES_ENABLED", True)
diff --git a/mlflow/server/handlers.py b/mlflow/server/handlers.py
index d41e4db378..31a8097187 100644
--- a/mlflow/server/handlers.py
+++ b/mlflow/server/handlers.py
@@ -16,14 +16,11 @@ from google.protobuf import descriptor
 from google.protobuf.json_format import ParseError
 
 from mlflow.entities import DatasetInput, ExperimentTag, FileInfo, Metric, Param, RunTag, ViewType
 from mlflow.entities.model_registry import ModelVersionTag, RegisteredModelTag
 from mlflow.entities.multipart_upload import MultipartUploadPart
-from mlflow.environment_variables import (
-    MLFLOW_ALLOW_FILE_URI_AS_MODEL_VERSION_SOURCE,
-    MLFLOW_DEPLOYMENTS_TARGET,
-)
+from mlflow.environment_variables import MLFLOW_DEPLOYMENTS_TARGET
 from mlflow.exceptions import MlflowException, _UnsupportedMultipartUploadException
 from mlflow.models import Model
 from mlflow.protos import databricks_pb2
 from mlflow.protos.databricks_pb2 import (
     INVALID_PARAMETER_VALUE,
@@ -103,11 +100,11 @@ from mlflow.tracking.registry import UnsupportedModelRegistryStoreURIException
 from mlflow.utils.file_utils import local_file_uri_to_path
 from mlflow.utils.mime_type_utils import _guess_mime_type
 from mlflow.utils.promptlab_utils import _create_promptlab_run_impl
 from mlflow.utils.proto_json_utils import message_to_json, parse_dict
 from mlflow.utils.string_utils import is_string_type
-from mlflow.utils.uri import is_file_uri, is_local_uri, validate_path_is_safe, validate_query_string
+from mlflow.utils.uri import is_local_uri, validate_path_is_safe, validate_query_string
 from mlflow.utils.validation import _validate_batch_log_api_req
 
 _logger = logging.getLogger(__name__)
 _tracking_store = None
 _model_registry_store = None
@@ -1602,22 +1599,10 @@ def _validate_source(source: str, run_id: str) -> None:
             "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} environment variable to "
-            "True.",
-            INVALID_PARAMETER_VALUE,
-        )
-
     # Checks if relative paths are present in the source (a security threat). If any are present,
     # raises an Exception.
     _validate_non_local_source_contains_relative_paths(source)
 
 
diff --git a/mlflow/utils/uri.py b/mlflow/utils/uri.py
index 8d54df5994..c55b99cc9b 100644
--- a/mlflow/utils/uri.py
+++ b/mlflow/utils/uri.py
@@ -43,28 +43,36 @@ def is_local_uri(uri, is_tracking_or_registry_uri=True):
     parsed_uri = urllib.parse.urlparse(uri)
     scheme = parsed_uri.scheme
     if scheme == "":
         return True
 
-    if parsed_uri.hostname and not (
+    is_remote_hostname = parsed_uri.hostname and not (
         parsed_uri.hostname == "."
         or parsed_uri.hostname.startswith("localhost")
         or parsed_uri.hostname.startswith("127.0.0.1")
-    ):
-        return False
-
+    )
     if scheme == "file":
+        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>)."
+            )
         return True
 
+    if is_remote_hostname:
+        return False
+
     if is_windows() and len(scheme) == 1 and scheme.lower() == pathlib.Path(uri).drive.lower()[0]:
         return True
 
     return False
 
 
 def is_file_uri(uri):
-    return urllib.parse.urlparse(uri).scheme == "file"
+    scheme = urllib.parse.urlparse(uri).scheme
+    return scheme == "file"
 
 
 def is_http_uri(uri):
     scheme = urllib.parse.urlparse(uri).scheme
     return scheme == "http" or scheme == "https"
diff --git a/tests/artifacts/test_artifacts.py b/tests/artifacts/test_artifacts.py
index 1c7f964ed5..e0fb554cda 100644
--- a/tests/artifacts/test_artifacts.py
+++ b/tests/artifacts/test_artifacts.py
@@ -6,11 +6,11 @@ from unittest import mock
 
 import pytest
 
 import mlflow
 from mlflow.exceptions import MlflowException
-from mlflow.utils.file_utils import local_file_uri_to_path, mkdir, path_to_local_file_uri
+from mlflow.utils.file_utils import mkdir, path_to_local_file_uri
 from mlflow.utils.os import is_windows
 
 Artifact = namedtuple("Artifact", ["uri", "content"])
 
 
@@ -245,25 +245,5 @@ def test_log_artifact_windows_path_with_hostname(text_artifact):
             )
             assert (
                 rf"{experiment_test_1_artifact_location}\{run.info.run_id}"
                 rf"\artifacts\{text_artifact.artifact_name}" == local_path
             )
-
-    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
-            )
diff --git a/tests/store/tracking/test_file_store.py b/tests/store/tracking/test_file_store.py
index 98407a580d..234914599b 100644
--- a/tests/store/tracking/test_file_store.py
+++ b/tests/store/tracking/test_file_store.py
@@ -2074,12 +2074,12 @@ def _assert_create_run_appends_to_artifact_uri_path_correctly(
 @pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
         (
-            "file://my_server/my_path/my_sub_path",
-            "file://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}/{r}/artifacts",
         ),
         ("path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}/{r}/artifacts"),
         ("/path/to/local/folder", "file:///{drive}path/to/local/folder/{e}/{r}/artifacts"),
         ("#path/to/local/folder?", "file://{cwd}/{e}/{r}/artifacts#path/to/local/folder?"),
         (
@@ -2177,11 +2177,11 @@ def _assert_create_experiment_appends_to_artifact_uri_path_correctly(
 
 @pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
-        ("file://my_server/my_path/my_sub_path", "file://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}"),
         ("path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}"),
         ("/path/to/local/folder", "file:///{drive}path/to/local/folder/{e}"),
         ("#path/to/local/folder?", "file://{cwd}/{e}#path/to/local/folder?"),
         ("file:path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}"),
         ("file:///path/to/local/folder", "file:///{drive}path/to/local/folder/{e}"),
diff --git a/tests/store/tracking/test_sqlalchemy_store.py b/tests/store/tracking/test_sqlalchemy_store.py
index 38233f86df..67e4cf80f7 100644
--- a/tests/store/tracking/test_sqlalchemy_store.py
+++ b/tests/store/tracking/test_sqlalchemy_store.py
@@ -3736,11 +3736,11 @@ def _assert_create_experiment_appends_to_artifact_uri_path_correctly(
 
 @pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
-        ("file://my_server/my_path/my_sub_path", "file://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}"),
         ("path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}"),
         ("/path/to/local/folder", "file:///{drive}path/to/local/folder/{e}"),
         ("#path/to/local/folder?", "file://{cwd}/{e}#path/to/local/folder?"),
         ("file:path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}"),
         ("file:///path/to/local/folder", "file:///{drive}path/to/local/folder/{e}"),
@@ -3839,12 +3839,12 @@ def _assert_create_run_appends_to_artifact_uri_path_correctly(
 @pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
         (
-            "file://my_server/my_path/my_sub_path",
-            "file://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}/{r}/artifacts",
         ),
         ("path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}/{r}/artifacts"),
         ("/path/to/local/folder", "file:///{drive}path/to/local/folder/{e}/{r}/artifacts"),
         ("#path/to/local/folder?", "file://{cwd}/{e}/{r}/artifacts#path/to/local/folder?"),
         ("file:path/to/local/folder", "file://{cwd}/path/to/local/folder/{e}/{r}/artifacts"),
diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py
index 1ed1686724..c2043acc61 100644
--- a/tests/tracking/test_rest_tracking.py
+++ b/tests/tracking/test_rest_tracking.py
@@ -1387,36 +1387,12 @@ def test_create_model_version_with_file_uri(mlflow_client):
             "name": name,
             "source": "file://123.456.789.123/path/to/source",
             "run_id": run.info.run_id,
         },
     )
-    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
+    assert response.status_code == 500, response.json()
+    assert "is not a valid remote uri" in response.json()["message"]
 
 
 def test_logging_model_with_local_artifact_uri(mlflow_client):
     from sklearn.linear_model import LogisticRegression
 
diff --git a/tests/utils/test_file_utils.py b/tests/utils/test_file_utils.py
index 37492d9dad..c5752b84f5 100644
--- a/tests/utils/test_file_utils.py
+++ b/tests/utils/test_file_utils.py
@@ -338,11 +338,10 @@ def test_handle_readonly_on_windows(tmp_path):
 
 @pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
 @pytest.mark.parametrize(
     ("input_uri", "expected_path"),
     [
-        ("file://my_server/my_path/my_sub_path", r"\\my_server\my_path\my_sub_path"),
         (r"\\my_server\my_path\my_sub_path", r"\\my_server\my_path\my_sub_path"),
     ],
 )
 def test_local_file_uri_to_path_on_windows(input_uri, expected_path):
     assert local_file_uri_to_path(input_uri) == expected_path
diff --git a/tests/utils/test_uri.py b/tests/utils/test_uri.py
index 836541a52d..ca9117131e 100644
--- a/tests/utils/test_uri.py
+++ b/tests/utils/test_uri.py
@@ -101,17 +101,19 @@ def test_is_local_uri():
     assert is_local_uri("file://127.0.0.1/mlruns")
     assert is_local_uri("file://127.0.0.1:5000/mlruns")
     assert is_local_uri("//proc/self/root")
     assert is_local_uri("/proc/self/root")
 
-    assert not is_local_uri("file://myhostname/path/to/file")
     assert not is_local_uri("https://whatever")
     assert not is_local_uri("http://whatever")
     assert not is_local_uri("databricks")
     assert not is_local_uri("databricks:whatever")
     assert not is_local_uri("databricks://whatever")
 
+    with pytest.raises(MlflowException, match="is not a valid remote uri."):
+        is_local_uri("file://myhostname/path/to/file")
+
 
 @pytest.mark.skipif(not is_windows(), reason="Windows-only test")
 def test_is_local_uri_windows():
     assert is_local_uri("C:\\foo\\mlruns")
     assert is_local_uri("C:/foo/mlruns")
@@ -680,11 +682,11 @@ def _assert_resolve_uri_if_local(input_uri, expected_uri):
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
         ("my/path", "{cwd}/my/path"),
         ("#my/path?a=b", "{cwd}/#my/path?a=b"),
-        ("file://myhostname/my/path", "file://myhostname/my/path"),
+        ("file://localhost/my/path", "file://localhost/my/path"),
         ("file:///my/path", "file:///{drive}my/path"),
         ("file:my/path", "file://{cwd}/my/path"),
         ("/home/my/path", "/home/my/path"),
         ("dbfs://databricks/a/b", "dbfs://databricks/a/b"),
         ("s3://host/my/path", "s3://host/my/path"),
@@ -698,11 +700,11 @@ def test_resolve_uri_if_local(input_uri, expected_uri):
 @pytest.mark.parametrize(
     ("input_uri", "expected_uri"),
     [
         ("my/path", "file://{cwd}/my/path"),
         ("#my/path?a=b", "file://{cwd}/#my/path?a=b"),
-        ("file://myhostname/my/path", "file://myhostname/my/path"),
+        ("\\myhostname/my/path", "file:///{drive}myhostname/my/path"),
         ("file:///my/path", "file:///{drive}my/path"),
         ("file:my/path", "file://{cwd}/my/path"),
         ("/home/my/path", "file:///{drive}home/my/path"),
         ("dbfs://databricks/a/b", "dbfs://databricks/a/b"),
         ("s3://host/my/path", "s3://host/my/path"),

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"),