CVE-2023-6831 (Python, published 2023-12-15T01:15Z)
Description: value: Path Traversal: '\..\filename' in GitHub repository mlflow/mlflow prior to 2.9.2.
Severity: nan (CVSS v3 base score: 8.1)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/mlflow/mlflow
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 message: Prevent path traversal with encoded URL (#10650)

Signed-off-by: B-Step62 <yuki.watanabe@databricks.com>

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)