CVE-2023-6831: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Path Traversal: '\..\filename' in GitHub repository mlflow/mlflow prior to 2.9.2.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 8.1
Published: 2023-12-15T01:15Z
Repository: https://github.com/mlflow/mlflow
Language: Python
Files changed: mlflow/utils/uri.py, tests/server/test_handlers.py, tests/store/artifact/test_http_artifact_repo.py, tests/tracking/test_rest_tracking.py, tests/utils/test_uri.py
Commit date: 2023-12-08 15:00:19 +0900
Commit message: Prevent path traversal with encoded URL (#10650)

Signed-off-by: B-Step62 <yuki.watanabe@databricks.com>
Diff: diff --git a/mlflow/utils/uri.py b/mlflow/utils/uri.py
index 331648b8e4..ecaa228c73 100644
--- a/mlflow/utils/uri.py
+++ b/mlflow/utils/uri.py
@@ -428,11 +428,14 @@ def validate_path_is_safe(path):
         not contain .. to navigate to parent dir in path
         not be an absolute path
     """
     from mlflow.utils.file_utils import local_file_uri_to_path
 
-    exc = MlflowException(f"Invalid path: {path}", error_code=INVALID_PARAMETER_VALUE)
+    # We must decode URL before validating it
+    path = urllib.parse.unquote(path)
+
+    exc = MlflowException("Invalid path", error_code=INVALID_PARAMETER_VALUE)
     if any((s in path) for s in ("#", "%23")):
         raise exc
 
     if is_file_uri(path):
         path = local_file_uri_to_path(path)
diff --git a/tests/server/test_handlers.py b/tests/server/test_handlers.py
index 1bc5bd0474..a6bbbace86 100644
--- a/tests/server/test_handlers.py
+++ b/tests/server/test_handlers.py
@@ -36,15 +36,16 @@ from mlflow.protos.model_registry_pb2 import (
     TransitionModelVersionStage,
     UpdateModelVersion,
     UpdateRegisteredModel,
 )
 from mlflow.protos.service_pb2 import CreateExperiment, SearchRuns
-from mlflow.server import BACKEND_STORE_URI_ENV_VAR, app
+from mlflow.server import BACKEND_STORE_URI_ENV_VAR, SERVE_ARTIFACTS_ENV_VAR, app
 from mlflow.server.handlers import (
     _create_experiment,
     _create_model_version,
     _create_registered_model,
+    _delete_artifact_mlflow_artifacts,
     _delete_model_version,
     _delete_model_version_tag,
     _delete_registered_model,
     _delete_registered_model_alias,
     _delete_registered_model_tag,
@@ -103,10 +104,15 @@ def mock_model_registry_store():
         mock_store = mock.MagicMock()
         m.return_value = mock_store
         yield mock_store
 
 
+@pytest.fixture
+def enable_serve_artifacts(monkeypatch):
+    monkeypatch.setenv(SERVE_ARTIFACTS_ENV_VAR, "true")
+
+
 def test_health():
     with app.test_client() as c:
         response = c.get("/health")
         assert response.status_code == 200
         assert response.get_data().decode() == "OK"
@@ -775,5 +781,24 @@ def test_get_model_version_by_alias(mock_get_request_message, mock_model_registr
     mock_model_registry_store.get_model_version_by_alias.return_value = mvd
     resp = _get_model_version_by_alias()
     _, args = mock_model_registry_store.get_model_version_by_alias.call_args
     assert args == {"name": name, "alias": alias}
     assert json.loads(resp.get_data()) == {"model_version": jsonify(mvd)}
+
+
+@pytest.mark.parametrize(
+    "path",
+    [
+        "/path",
+        "path/../to/file",
+        "/etc/passwd",
+        "/etc/passwd%00.jpg",
+        "/file://etc/passwd",
+        "%2E%2E%2F%2E%2E%2Fpath",
+    ],
+)
+def test_delete_artifact_mlflow_artifacts_throws_for_malicious_path(enable_serve_artifacts, path):
+    response = _delete_artifact_mlflow_artifacts(path)
+    assert response.status_code == 400
+    json_response = json.loads(response.get_data())
+    assert json_response["error_code"] == ErrorCode.Name(INVALID_PARAMETER_VALUE)
+    assert json_response["message"] == "Invalid path"
diff --git a/tests/store/artifact/test_http_artifact_repo.py b/tests/store/artifact/test_http_artifact_repo.py
index 8249f083d6..f6b9a9d72f 100644
--- a/tests/store/artifact/test_http_artifact_repo.py
+++ b/tests/store/artifact/test_http_artifact_repo.py
@@ -244,11 +244,11 @@ def test_list_artifacts(http_artifact_repo):
     ):
         with pytest.raises(Exception, match="request failed"):
             http_artifact_repo.list_artifacts()
 
 
-@pytest.mark.parametrize("path", ["/tmp/path", "../../path"])
+@pytest.mark.parametrize("path", ["/tmp/path", "../../path", "%2E%2E%2Fpath"])
 def test_list_artifacts_malicious_path(http_artifact_repo, path):
     with mock.patch(
         "mlflow.store.artifact.http_artifact_repo.http_request",
         return_value=MockResponse(
             {
@@ -257,11 +257,11 @@ def test_list_artifacts_malicious_path(http_artifact_repo, path):
                 ]
             },
             200,
         ),
     ):
-        with pytest.raises(MlflowException, match=f"Invalid path: {path}"):
+        with pytest.raises(MlflowException, match="Invalid path"):
             http_artifact_repo.list_artifacts()
 
 
 def read_file(path):
     with open(path) as f:
diff --git a/tests/tracking/test_rest_tracking.py b/tests/tracking/test_rest_tracking.py
index d56eecd778..5f952dda4a 100644
--- a/tests/tracking/test_rest_tracking.py
+++ b/tests/tracking/test_rest_tracking.py
@@ -33,11 +33,10 @@ from mlflow.entities import (
     RunTag,
     ViewType,
 )
 from mlflow.exceptions import MlflowException
 from mlflow.models import Model
-from mlflow.server.handlers import validate_path_is_safe
 from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
 from mlflow.utils import mlflow_tags
 from mlflow.utils.file_utils import TempDir, path_to_local_file_uri
 from mlflow.utils.mlflow_tags import (
     MLFLOW_DATASET_CONTEXT,
@@ -512,167 +511,21 @@ def test_set_tag_validation(mlflow_client):
         },
     )
     assert response.status_code == 200
 
 
-@pytest.mark.parametrize(
-    "path",
-    [
-        "path",
-        "path/",
-        "path/to/file",
-    ],
-)
-def test_validate_path_is_safe_good(path):
-    validate_path_is_safe(path)
-
-
-@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
-@pytest.mark.parametrize(
-    "path",
-    [
-        # relative path from current directory of C: drive
-        ".../...//",
-    ],
-)
-def test_validate_path_is_safe_windows_good(path):
-    validate_path_is_safe(path)
-
-
-@pytest.mark.skipif(is_windows(), reason="This test does not pass on Windows")
-@pytest.mark.parametrize(
-    "path",
-    [
-        "/path",
-        "../path",
-        "../../path",
-        "./../path",
-        "path/../to/file",
-        "path/../../to/file",
-        "file://a#/..//tmp",
-        "file://a%23/..//tmp/",
-        "/etc/passwd",
-        "/etc/passwd%00.jpg",
-        "/etc/passwd%00.html",
-        "/etc/passwd%00.txt",
-        "/etc/passwd%00.php",
-        "/etc/passwd%00.asp",
-        "/file://etc/passwd",
-    ],
-)
-def test_validate_path_is_safe_bad(path):
-    with pytest.raises(MlflowException, match="Invalid path"):
-        validate_path_is_safe(path)
-
-
-@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
-@pytest.mark.parametrize(
-    "path",
-    [
-        r"../path",
-        r"../../path",
-        r"./../path",
-        r"path/../to/file",
-        r"path/../../to/file",
-        r"..\path",
-        r"..\..\path",
-        r".\..\path",
-        r"path\..\to\file",
-        r"path\..\..\to\file",
-        # Drive-relative paths
-        r"C:path",
-        r"C:path/",
-        r"C:path/to/file",
-        r"C:../path/to/file",
-        r"C:\path",
-        r"C:/path",
-        r"C:\path\to\file",
-        r"C:\path/to/file",
-        r"C:\path\..\to\file",
-        r"C:/path/../to/file",
-        # UNC(Universal Naming Convention) paths
-        r"\\path\to\file",
-        r"\\path/to/file",
-        r"\\.\\C:\path\to\file",
-        r"\\?\C:\path\to\file",
-        r"\\?\UNC/path/to/file",
-        # Other potential attackable paths
-        r"/etc/password",
-        r"/path",
-        r"/etc/passwd%00.jpg",
-        r"/etc/passwd%00.html",
-        r"/etc/passwd%00.txt",
-        r"/etc/passwd%00.php",
-        r"/etc/passwd%00.asp",
-        r"/Windows/no/such/path",
-        r"/file://etc/passwd",
-        r"/file:c:/passwd",
-        r"/file://d:/windows/win.ini",
-        r"/file://./windows/win.ini",
-        r"file://c:/boot.ini",
-        r"file://C:path",
-        r"file://C:path/",
-        r"file://C:path/to/file",
-        r"file:///C:/Windows/System32/",
-        r"file:///etc/passwd",
-        r"file:///d:/windows/repair/sam",
-        r"file:///proc/version",
-        r"file:///inetpub/wwwroot/global.asa",
-        r"/file://../windows/win.ini",
-        r"../etc/passwd",
-        r"..\Windows\System32\\",
-        r"C:\Windows\System32\\",
-        r"/etc/passwd",
-        r"::Windows\System32",
-        r"..\..\..\..\Windows\System32\\",
-        r"../Windows/System32",
-        r"....\\",
-        r"\\?\C:\Windows\System32\\",
-        r"\\.\C:\Windows\System32\\",
-        r"\\UNC\Server\Share\\",
-        r"\\Server\Share\folder\\",
-        r"\\127.0.0.1\c$\Windows\\",
-        r"\\localhost\c$\Windows\\",
-        r"\\smbserver\share\path\\",
-        r"..\\?\C:\Windows\System32\\",
-        r"C:/Windows/../Windows/System32/",
-        r"C:\Windows\..\Windows\System32\\",
-        r"../../../../../../../../../../../../Windows/System32",
-        r"../../../../../../../../../../../../etc/passwd",
-        r"../../../../../../../../../../../../var/www/html/index.html",
-        r"../../../../../../../../../../../../usr/local/etc/openvpn/server.conf",
-        r"../../../../../../../../../../../../Program Files (x86)",
-        r"/../../../../../../../../../../../../Windows/System32",
-        r"/Windows\../etc/passwd",
-        r"/Windows\..\Windows\System32\\",
-        r"/Windows\..\Windows\System32\cmd.exe",
-        r"/Windows\..\Windows\System32\msconfig.exe",
-        r"/Windows\..\Windows\System32\regedit.exe",
-        r"/Windows\..\Windows\System32\taskmgr.exe",
-        r"/Windows\..\Windows\System32\control.exe",
-        r"/Windows\..\Windows\System32\services.msc",
-        r"/Windows\..\Windows\System32\diskmgmt.msc",
-        r"/Windows\..\Windows\System32\eventvwr.msc",
-        r"/Windows/System32/drivers/etc/hosts",
-    ],
-)
-def test_validate_path_is_safe_windows_bad(path):
-    with pytest.raises(MlflowException, match="Invalid path"):
-        validate_path_is_safe(path)
-
-
 def test_path_validation(mlflow_client):
     experiment_id = mlflow_client.create_experiment("tags validation")
     created_run = mlflow_client.create_run(experiment_id)
     run_id = created_run.info.run_id
     invalid_path = "../path"
 
     def assert_response(resp):
         assert resp.status_code == 400
         assert response.json() == {
             "error_code": "INVALID_PARAMETER_VALUE",
-            "message": f"Invalid path: {invalid_path}",
+            "message": "Invalid path",
         }
 
     response = requests.get(
         f"{mlflow_client.tracking_uri}/api/2.0/mlflow/artifacts/list",
         params={"run_id": run_id, "path": invalid_path},
diff --git a/tests/utils/test_uri.py b/tests/utils/test_uri.py
index b776795c61..26aecc5146 100644
--- a/tests/utils/test_uri.py
+++ b/tests/utils/test_uri.py
@@ -22,10 +22,11 @@ from mlflow.utils.uri import (
     is_http_uri,
     is_local_uri,
     is_valid_dbfs_uri,
     remove_databricks_profile_info_from_artifact_uri,
     resolve_uri_if_local,
+    validate_path_is_safe,
 )
 
 
 def test_extract_db_type_from_uri():
     uri = "{}://username:password@host:port/database"
@@ -722,5 +723,154 @@ def test_correctly_detect_fuse_and_uc_uris(uri):
         "/fuse-mounts/my_path",
     ],
 )
 def test_negative_detection(uri):
     assert not is_fuse_or_uc_volumes_uri(uri)
+
+
+@pytest.mark.parametrize(
+    "path",
+    [
+        "path",
+        "path/",
+        "path/to/file",
+    ],
+)
+def test_validate_path_is_safe_good(path):
+    validate_path_is_safe(path)
+
+
+@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
+@pytest.mark.parametrize(
+    "path",
+    [
+        # relative path from current directory of C: drive
+        ".../...//",
+    ],
+)
+def test_validate_path_is_safe_windows_good(path):
+    validate_path_is_safe(path)
+
+
+@pytest.mark.skipif(is_windows(), reason="This test does not pass on Windows")
+@pytest.mark.parametrize(
+    "path",
+    [
+        "/path",
+        "../path",
+        "../../path",
+        "./../path",
+        "path/../to/file",
+        "path/../../to/file",
+        "file://a#/..//tmp",
+        "file://a%23/..//tmp/",
+        "/etc/passwd",
+        "/etc/passwd%00.jpg",
+        "/etc/passwd%00.html",
+        "/etc/passwd%00.txt",
+        "/etc/passwd%00.php",
+        "/etc/passwd%00.asp",
+        "/file://etc/passwd",
+        # Encoded paths with '..'
+        "%2E%2E%2Fpath",
+        "%2E%2E%2F%2E%2E%2Fpath",
+    ],
+)
+def test_validate_path_is_safe_bad(path):
+    with pytest.raises(MlflowException, match="Invalid path"):
+        validate_path_is_safe(path)
+
+
+@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
+@pytest.mark.parametrize(
+    "path",
+    [
+        r"../path",
+        r"../../path",
+        r"./../path",
+        r"path/../to/file",
+        r"path/../../to/file",
+        r"..\path",
+        r"..\..\path",
+        r".\..\path",
+        r"path\..\to\file",
+        r"path\..\..\to\file",
+        # Drive-relative paths
+        r"C:path",
+        r"C:path/",
+        r"C:path/to/file",
+        r"C:../path/to/file",
+        r"C:\path",
+        r"C:/path",
+        r"C:\path\to\file",
+        r"C:\path/to/file",
+        r"C:\path\..\to\file",
+        r"C:/path/../to/file",
+        # UNC(Universal Naming Convention) paths
+        r"\\path\to\file",
+        r"\\path/to/file",
+        r"\\.\\C:\path\to\file",
+        r"\\?\C:\path\to\file",
+        r"\\?\UNC/path/to/file",
+        # Other potential attackable paths
+        r"/etc/password",
+        r"/path",
+        r"/etc/passwd%00.jpg",
+        r"/etc/passwd%00.html",
+        r"/etc/passwd%00.txt",
+        r"/etc/passwd%00.php",
+        r"/etc/passwd%00.asp",
+        r"/Windows/no/such/path",
+        r"/file://etc/passwd",
+        r"/file:c:/passwd",
+        r"/file://d:/windows/win.ini",
+        r"/file://./windows/win.ini",
+        r"file://c:/boot.ini",
+        r"file://C:path",
+        r"file://C:path/",
+        r"file://C:path/to/file",
+        r"file:///C:/Windows/System32/",
+        r"file:///etc/passwd",
+        r"file:///d:/windows/repair/sam",
+        r"file:///proc/version",
+        r"file:///inetpub/wwwroot/global.asa",
+        r"/file://../windows/win.ini",
+        r"../etc/passwd",
+        r"..\Windows\System32\\",
+        r"C:\Windows\System32\\",
+        r"/etc/passwd",
+        r"::Windows\System32",
+        r"..\..\..\..\Windows\System32\\",
+        r"../Windows/System32",
+        r"....\\",
+        r"\\?\C:\Windows\System32\\",
+        r"\\.\C:\Windows\System32\\",
+        r"\\UNC\Server\Share\\",
+        r"\\Server\Share\folder\\",
+        r"\\127.0.0.1\c$\Windows\\",
+        r"\\localhost\c$\Windows\\",
+        r"\\smbserver\share\path\\",
+        r"..\\?\C:\Windows\System32\\",
+        r"C:/Windows/../Windows/System32/",
+        r"C:\Windows\..\Windows\System32\\",
+        r"../../../../../../../../../../../../Windows/System32",
+        r"../../../../../../../../../../../../etc/passwd",
+        r"../../../../../../../../../../../../var/www/html/index.html",
+        r"../../../../../../../../../../../../usr/local/etc/openvpn/server.conf",
+        r"../../../../../../../../../../../../Program Files (x86)",
+        r"/../../../../../../../../../../../../Windows/System32",
+        r"/Windows\../etc/passwd",
+        r"/Windows\..\Windows\System32\\",
+        r"/Windows\..\Windows\System32\cmd.exe",
+        r"/Windows\..\Windows\System32\msconfig.exe",
+        r"/Windows\..\Windows\System32\regedit.exe",
+        r"/Windows\..\Windows\System32\taskmgr.exe",
+        r"/Windows\..\Windows\System32\control.exe",
+        r"/Windows\..\Windows\System32\services.msc",
+        r"/Windows\..\Windows\System32\diskmgmt.msc",
+        r"/Windows\..\Windows\System32\eventvwr.msc",
+        r"/Windows/System32/drivers/etc/hosts",
+    ],
+)
+def test_validate_path_is_safe_windows_bad(path):
+    with pytest.raises(MlflowException, match="Invalid path"):
+        validate_path_is_safe(path)

Vulnerable code:     exc = MlflowException(f"Invalid path: {path}", error_code=INVALID_PARAMETER_VALUE)
from mlflow.server import BACKEND_STORE_URI_ENV_VAR, app
@pytest.mark.parametrize("path", ["/tmp/path", "../../path"])
        with pytest.raises(MlflowException, match=f"Invalid path: {path}"):
from mlflow.server.handlers import validate_path_is_safe
@pytest.mark.parametrize(
    "path",
    [
        "path",
        "path/",
        "path/to/file",
    ],
)
def test_validate_path_is_safe_good(path):
    validate_path_is_safe(path)


@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
@pytest.mark.parametrize(
    "path",
    [
        # relative path from current directory of C: drive
        ".../...//",
    ],
)
def test_validate_path_is_safe_windows_good(path):
    validate_path_is_safe(path)


@pytest.mark.skipif(is_windows(), reason="This test does not pass on Windows")
@pytest.mark.parametrize(
    "path",
    [
        "/path",
        "../path",
        "../../path",
        "./../path",
        "path/../to/file",
        "path/../../to/file",
        "file://a#/..//tmp",
        "file://a%23/..//tmp/",
        "/etc/passwd",
        "/etc/passwd%00.jpg",
        "/etc/passwd%00.html",
        "/etc/passwd%00.txt",
        "/etc/passwd%00.php",
        "/etc/passwd%00.asp",
        "/file://etc/passwd",
    ],
)
def test_validate_path_is_safe_bad(path):
    with pytest.raises(MlflowException, match="Invalid path"):
        validate_path_is_safe(path)


@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
@pytest.mark.parametrize(
    "path",
    [
        r"../path",
        r"../../path",
        r"./../path",
        r"path/../to/file",
        r"path/../../to/file",
        r"..\path",
        r"..\..\path",
        r".\..\path",
        r"path\..\to\file",
        r"path\..\..\to\file",
        # Drive-relative paths
        r"C:path",
        r"C:path/",
        r"C:path/to/file",
        r"C:../path/to/file",
        r"C:\path",
        r"C:/path",
        r"C:\path\to\file",
        r"C:\path/to/file",
        r"C:\path\..\to\file",
        r"C:/path/../to/file",
        # UNC(Universal Naming Convention) paths
        r"\\path\to\file",
        r"\\path/to/file",
        r"\\.\\C:\path\to\file",
        r"\\?\C:\path\to\file",
        r"\\?\UNC/path/to/file",
        # Other potential attackable paths
        r"/etc/password",
        r"/path",
        r"/etc/passwd%00.jpg",
        r"/etc/passwd%00.html",
        r"/etc/passwd%00.txt",
        r"/etc/passwd%00.php",
        r"/etc/passwd%00.asp",
        r"/Windows/no/such/path",
        r"/file://etc/passwd",
        r"/file:c:/passwd",
        r"/file://d:/windows/win.ini",
        r"/file://./windows/win.ini",
        r"file://c:/boot.ini",
        r"file://C:path",
        r"file://C:path/",
        r"file://C:path/to/file",
        r"file:///C:/Windows/System32/",
        r"file:///etc/passwd",
        r"file:///d:/windows/repair/sam",
        r"file:///proc/version",
        r"file:///inetpub/wwwroot/global.asa",
        r"/file://../windows/win.ini",
        r"../etc/passwd",
        r"..\Windows\System32\\",
        r"C:\Windows\System32\\",
        r"/etc/passwd",
        r"::Windows\System32",
        r"..\..\..\..\Windows\System32\\",
        r"../Windows/System32",
        r"....\\",
        r"\\?\C:\Windows\System32\\",
        r"\\.\C:\Windows\System32\\",
        r"\\UNC\Server\Share\\",
        r"\\Server\Share\folder\\",
        r"\\127.0.0.1\c$\Windows\\",
        r"\\localhost\c$\Windows\\",
        r"\\smbserver\share\path\\",
        r"..\\?\C:\Windows\System32\\",
        r"C:/Windows/../Windows/System32/",
        r"C:\Windows\..\Windows\System32\\",
        r"../../../../../../../../../../../../Windows/System32",
        r"../../../../../../../../../../../../etc/passwd",
        r"../../../../../../../../../../../../var/www/html/index.html",
        r"../../../../../../../../../../../../usr/local/etc/openvpn/server.conf",
        r"../../../../../../../../../../../../Program Files (x86)",
        r"/../../../../../../../../../../../../Windows/System32",
        r"/Windows\../etc/passwd",
        r"/Windows\..\Windows\System32\\",
        r"/Windows\..\Windows\System32\cmd.exe",
        r"/Windows\..\Windows\System32\msconfig.exe",
        r"/Windows\..\Windows\System32\regedit.exe",
        r"/Windows\..\Windows\System32\taskmgr.exe",
        r"/Windows\..\Windows\System32\control.exe",
        r"/Windows\..\Windows\System32\services.msc",
        r"/Windows\..\Windows\System32\diskmgmt.msc",
        r"/Windows\..\Windows\System32\eventvwr.msc",
        r"/Windows/System32/drivers/etc/hosts",
    ],
)
def test_validate_path_is_safe_windows_bad(path):
    with pytest.raises(MlflowException, match="Invalid path"):
        validate_path_is_safe(path)


            "message": f"Invalid path: {invalid_path}",
Fixed code:     # We must decode URL before validating it
    path = urllib.parse.unquote(path)

    exc = MlflowException("Invalid path", error_code=INVALID_PARAMETER_VALUE)
from mlflow.server import BACKEND_STORE_URI_ENV_VAR, SERVE_ARTIFACTS_ENV_VAR, app
    _delete_artifact_mlflow_artifacts,
@pytest.fixture
def enable_serve_artifacts(monkeypatch):
    monkeypatch.setenv(SERVE_ARTIFACTS_ENV_VAR, "true")




@pytest.mark.parametrize(
    "path",
    [
        "/path",
        "path/../to/file",
        "/etc/passwd",
        "/etc/passwd%00.jpg",
        "/file://etc/passwd",
        "%2E%2E%2F%2E%2E%2Fpath",
    ],
)
def test_delete_artifact_mlflow_artifacts_throws_for_malicious_path(enable_serve_artifacts, path):
    response = _delete_artifact_mlflow_artifacts(path)
    assert response.status_code == 400
    json_response = json.loads(response.get_data())
    assert json_response["error_code"] == ErrorCode.Name(INVALID_PARAMETER_VALUE)
    assert json_response["message"] == "Invalid path"
@pytest.mark.parametrize("path", ["/tmp/path", "../../path", "%2E%2E%2Fpath"])
        with pytest.raises(MlflowException, match="Invalid path"):
            "message": "Invalid path",
    validate_path_is_safe,


@pytest.mark.parametrize(
    "path",
    [
        "path",
        "path/",
        "path/to/file",
    ],
)
def test_validate_path_is_safe_good(path):
    validate_path_is_safe(path)


@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
@pytest.mark.parametrize(
    "path",
    [
        # relative path from current directory of C: drive
        ".../...//",
    ],
)
def test_validate_path_is_safe_windows_good(path):
    validate_path_is_safe(path)


@pytest.mark.skipif(is_windows(), reason="This test does not pass on Windows")
@pytest.mark.parametrize(
    "path",
    [
        "/path",
        "../path",
        "../../path",
        "./../path",
        "path/../to/file",
        "path/../../to/file",
        "file://a#/..//tmp",
        "file://a%23/..//tmp/",
        "/etc/passwd",
        "/etc/passwd%00.jpg",
        "/etc/passwd%00.html",
        "/etc/passwd%00.txt",
        "/etc/passwd%00.php",
        "/etc/passwd%00.asp",
        "/file://etc/passwd",
        # Encoded paths with '..'
        "%2E%2E%2Fpath",
        "%2E%2E%2F%2E%2E%2Fpath",
    ],
)
def test_validate_path_is_safe_bad(path):
    with pytest.raises(MlflowException, match="Invalid path"):
        validate_path_is_safe(path)


@pytest.mark.skipif(not is_windows(), reason="This test only passes on Windows")
@pytest.mark.parametrize(
    "path",
    [
        r"../path",
        r"../../path",
        r"./../path",
        r"path/../to/file",
        r"path/../../to/file",
        r"..\path",
        r"..\..\path",
        r".\..\path",
        r"path\..\to\file",
        r"path\..\..\to\file",
        # Drive-relative paths
        r"C:path",
        r"C:path/",
        r"C:path/to/file",
        r"C:../path/to/file",
        r"C:\path",
        r"C:/path",
        r"C:\path\to\file",
        r"C:\path/to/file",
        r"C:\path\..\to\file",
        r"C:/path/../to/file",
        # UNC(Universal Naming Convention) paths
        r"\\path\to\file",
        r"\\path/to/file",
        r"\\.\\C:\path\to\file",
        r"\\?\C:\path\to\file",
        r"\\?\UNC/path/to/file",
        # Other potential attackable paths
        r"/etc/password",
        r"/path",
        r"/etc/passwd%00.jpg",
        r"/etc/passwd%00.html",
        r"/etc/passwd%00.txt",
        r"/etc/passwd%00.php",
        r"/etc/passwd%00.asp",
        r"/Windows/no/such/path",
        r"/file://etc/passwd",
        r"/file:c:/passwd",
        r"/file://d:/windows/win.ini",
        r"/file://./windows/win.ini",
        r"file://c:/boot.ini",
        r"file://C:path",
        r"file://C:path/",
        r"file://C:path/to/file",
        r"file:///C:/Windows/System32/",
        r"file:///etc/passwd",
        r"file:///d:/windows/repair/sam",
        r"file:///proc/version",
        r"file:///inetpub/wwwroot/global.asa",
        r"/file://../windows/win.ini",
        r"../etc/passwd",
        r"..\Windows\System32\\",
        r"C:\Windows\System32\\",
        r"/etc/passwd",
        r"::Windows\System32",
        r"..\..\..\..\Windows\System32\\",
        r"../Windows/System32",
        r"....\\",
        r"\\?\C:\Windows\System32\\",
        r"\\.\C:\Windows\System32\\",
        r"\\UNC\Server\Share\\",
        r"\\Server\Share\folder\\",
        r"\\127.0.0.1\c$\Windows\\",
        r"\\localhost\c$\Windows\\",
        r"\\smbserver\share\path\\",
        r"..\\?\C:\Windows\System32\\",
        r"C:/Windows/../Windows/System32/",
        r"C:\Windows\..\Windows\System32\\",
        r"../../../../../../../../../../../../Windows/System32",
        r"../../../../../../../../../../../../etc/passwd",
        r"../../../../../../../../../../../../var/www/html/index.html",
        r"../../../../../../../../../../../../usr/local/etc/openvpn/server.conf",
        r"../../../../../../../../../../../../Program Files (x86)",
        r"/../../../../../../../../../../../../Windows/System32",
        r"/Windows\../etc/passwd",
        r"/Windows\..\Windows\System32\\",
        r"/Windows\..\Windows\System32\cmd.exe",
        r"/Windows\..\Windows\System32\msconfig.exe",
        r"/Windows\..\Windows\System32\regedit.exe",
        r"/Windows\..\Windows\System32\taskmgr.exe",
        r"/Windows\..\Windows\System32\control.exe",
        r"/Windows\..\Windows\System32\services.msc",
        r"/Windows\..\Windows\System32\diskmgmt.msc",
        r"/Windows\..\Windows\System32\eventvwr.msc",
        r"/Windows/System32/drivers/etc/hosts",
    ],
)
def test_validate_path_is_safe_windows_bad(path):
    with pytest.raises(MlflowException, match="Invalid path"):
        validate_path_is_safe(path)