VYPR
High severity8.1NVD Advisory· Published Feb 20, 2026· Updated Apr 15, 2026

CVE-2026-2033

CVE-2026-2033

Description

MLflow Tracking Server Artifact Handler Directory Traversal Remote Code Execution Vulnerability. This vulnerability allows remote attackers to execute arbitrary code on affected installations of MLflow Tracking Server. Authentication is not required to exploit this vulnerability.

The specific flaw exists within the handling of artifact file paths. The issue results from the lack of proper validation of a user-supplied path prior to using it in file operations. An attacker can leverage this vulnerability to execute code in the context of the service account. Was ZDI-CAN-26649.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mlflowPyPI
< 3.8.0rc03.8.0rc0

Affected products

1

Patches

1
5bf2ec2bd422

Fix artifact path traversal vector (#19260)

https://github.com/mlflow/mlflowBen WilsonDec 10, 2025via ghsa
2 files changed · +80 3
  • mlflow/store/tracking/file_store.py+30 3 modified
    @@ -624,7 +624,10 @@ def _hard_delete_run(self, run_id):
             Permanently delete a run (metadata and metrics, tags, parameters).
             This is used by the ``mlflow gc`` command line and is not intended to be used elsewhere.
             """
    -        _, run_dir = self._find_run_root(run_id)
    +        # NB: Skip validation here since artifacts may have already been deleted
    +        # by gc before calling this method. The run_id was already validated
    +        # by search_runs/get_run before reaching this point.
    +        _, run_dir = self._find_run_root(run_id, validate_structure=False)
             shutil.rmtree(run_dir)
     
         def _get_deleted_runs(self, older_than=0):
    @@ -670,15 +673,33 @@ def _find_experiment_folder(self, run_path):
                 return get_parent_dir(parent)
             return parent
     
    -    def _find_run_root(self, run_uuid):
    +    def _is_valid_run_directory(self, run_dir):
    +        # Defense in depth: ensure we're not inside an artifacts folder
    +        path_parts = os.path.normpath(run_dir).split(os.sep)
    +        if FileStore.ARTIFACTS_FOLDER_NAME in path_parts[:-1]:
    +            return False
    +
    +        required_subdirs = [
    +            FileStore.METRICS_FOLDER_NAME,
    +            FileStore.PARAMS_FOLDER_NAME,
    +            FileStore.ARTIFACTS_FOLDER_NAME,
    +        ]
    +        return all(is_directory(os.path.join(run_dir, subdir)) for subdir in required_subdirs)
    +
    +    def _find_run_root(self, run_uuid, validate_structure=True):
             _validate_run_id(run_uuid)
             self._check_root_dir()
             all_experiments = self._get_active_experiments(True) + self._get_deleted_experiments(True)
             for experiment_dir in all_experiments:
                 runs = find(experiment_dir, run_uuid, full_path=True)
                 if len(runs) == 0:
                     continue
    -            return os.path.basename(os.path.abspath(experiment_dir)), runs[0]
    +            run_dir = runs[0]
    +            # NB: Validate run directory structure to prevent path traversal via malicious
    +            # meta.yaml in artifact folders (ZDI-CAN-26649)
    +            if validate_structure and not self._is_valid_run_directory(run_dir):
    +                continue
    +            return os.path.basename(os.path.abspath(experiment_dir)), run_dir
             return None, None
     
         def update_run_info(self, run_id, run_status, end_time, run_name):
    @@ -784,6 +805,12 @@ def _get_run_info(self, run_uuid):
                     f"Run '{run_uuid}' metadata is in invalid state.",
                     databricks_pb2.INVALID_STATE,
                 )
    +        # Defense in depth: verify run_id in meta.yaml matches the directory name
    +        if run_info.run_id != os.path.basename(run_dir):
    +            raise MlflowException(
    +                f"Run '{run_uuid}' metadata is in invalid state.",
    +                databricks_pb2.INVALID_STATE,
    +            )
             return run_info
     
         def _get_run_info_from_dir(self, run_dir):
    
  • tests/store/tracking/test_file_store.py+50 0 modified
    @@ -3987,3 +3987,53 @@ def test_get_experiment_missing_and_empty_metadata_file(tmp_path):
         # Should raise MissingConfigException about invalid metadata
         with pytest.raises(MissingConfigException, match=rf"Experiment {exp_id} is invalid with empty"):
             fs._get_experiment(exp_id)
    +
    +
    +def test_malicious_meta_yaml_in_artifact_folder_path_traversal(tmp_path):
    +    """
    +    Regression test for ZDI-CAN-26649: Directory traversal via malicious meta.yaml.
    +
    +    Attack flow that should be blocked:
    +    1. Create experiment with artifact_location pointing to FileStore root
    +    2. Create a run - artifacts go to {root}/{run_id}/artifacts/
    +    3. Plant malicious meta.yaml in artifacts folder with arbitrary artifact_uri
    +    4. Try to use "artifacts" as run_uuid to access files via the malicious artifact_uri
    +
    +    The fix validates that run directories have required subdirectories (metrics/, params/,
    +    artifacts/), which artifact folders do not have.
    +    """
    +    root_dir = tmp_path / "mlruns"
    +    root_dir.mkdir()
    +    fs = FileStore(str(root_dir))
    +
    +    exp_id = fs.create_experiment("malicious_exp", artifact_location=str(root_dir))
    +    run = fs.create_run(
    +        experiment_id=exp_id, user_id="attacker", start_time=0, tags=[], run_name=""
    +    )
    +    run_id = run.info.run_id
    +
    +    assert Path(run.info.artifact_uri) == root_dir / run_id / "artifacts"
    +
    +    artifacts_dir = root_dir / run_id / "artifacts"
    +    artifacts_dir.mkdir(parents=True, exist_ok=True)
    +
    +    target_dir = tmp_path / "sensitive_data"
    +    target_dir.mkdir()
    +
    +    malicious_meta = {
    +        "run_id": "artifacts",
    +        "run_uuid": "artifacts",
    +        "experiment_id": run_id,
    +        "user_id": "attacker",
    +        "status": 1,
    +        "start_time": 0,
    +        "end_time": None,
    +        "lifecycle_stage": "active",
    +        "artifact_uri": str(target_dir),
    +        "tags": [],
    +    }
    +    write_yaml(str(artifacts_dir), "meta.yaml", malicious_meta)
    +
    +    # The fix should prevent the artifact folder from being treated as a run directory
    +    with pytest.raises(MlflowException, match="Run 'artifacts' not found"):
    +        fs.get_run("artifacts")
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.