VAITP Dataset

← Back to the dataset

CVE-2025-15379

Command injection in MLflow via unsanitized dependencies in model artifacts.

  • CVSS 9.8
  • CWE-77
  • Input Validation and Sanitization
  • Local

A command injection vulnerability exists in MLflow's model serving container initialization code, specifically in the `_install_model_dependencies_to_env()` function. When deploying a model with `env_manager=LOCAL`, MLflow reads dependency specifications from the model artifact's `python_env.yaml` file and directly interpolates them into a shell command without sanitization. This allows an attacker to supply a malicious model artifact and achieve arbitrary command execution on systems that deploy the model. The vulnerability affects versions 3.8.0 and is fixed in version 3.8.2.

CVSS base score
9.8
Published
2026-03-30
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Command Injection
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
MLflow
Fixed by upgrading
Yes

Solution

Upgrade MLflow to version 3.8.2 or later.

Vulnerable code sample

import subprocess
import yaml
import os

def _install_model_dependencies_to_env(model_path):
    """
    A representative example of the vulnerable function in MLflow.
    This function reads dependencies from a `python_env.yaml` file
    and installs them using a shell command, without proper sanitization.
    
    To trigger the vulnerability, this function would be called on a directory
    containing a malicious python_env.yaml file like:
    
    dependencies:
      - pip:
        - "numpy; touch /tmp/pwned"
    """
    python_env_path = os.path.join(model_path, "python_env.yaml")

    if not os.path.exists(python_env_path):
        return

    with open(python_env_path, 'r') as f:
        env_config = yaml.safe_load(f)

    pip_dependencies = []
    if "dependencies" in env_config:
        for dependency in env_config["dependencies"]:
            if isinstance(dependency, dict) and "pip" in dependency:
                pip_dependencies.extend(dependency["pip"])

    if not pip_dependencies:
        return
    
    # VULNERABLE PART: The dependency strings are joined and directly
    # interpolated into the shell command without any sanitization.
    deps_to_install = " ".join(pip_dependencies)
    command = f"pip install {deps_to_install}"

    # The use of shell=True with the unsanitized `command` string 
    # allows for arbitrary command injection.
    subprocess.run(command, shell=True, check=True, text=True)

Patched code sample

import subprocess
import sys
from typing import List
import os

def _install_model_dependencies_to_env(dependencies: List[str]):
    """
    Represents the fixed version of the vulnerable MLflow function, demonstrating
    the fix for CVE-2025-15379.

    This function simulates installing model dependencies using the secure, patched
    method. The vulnerability existed in MLflow versions up to 3.8.0 and was fixed
    in 3.8.2.

    The fix involves ensuring that dependency specifications are not directly
    interpolated into a shell command. Instead, the command and its arguments are
    passed as a list to a subprocess, preventing the shell from interpreting
    any malicious characters within the dependency strings.

    Args:
        dependencies: A list of dependency strings, simulating the content
                      read from a model's `python_env.yaml` file.
    """
    pip_executable_path = "pip"

    # --- THE VULNERABLE METHOD (for context) ---
    # The vulnerable code in MLflow 3.8.0 would look something like this:
    #
    # vulnerable_cmd_str = f"pip install {' '.join(dependencies)}"
    # subprocess.run(vulnerable_cmd_str, shell=True)
    #
    # If a dependency was "pandas; touch /tmp/pwned", the `shell=True` argument
    # would cause the shell to execute `pip install pandas` and then `touch /tmp/pwned`.

    # --- THE FIXED METHOD (as in MLflow 3.8.2) ---
    # The command and its arguments are assembled into a list. Each dependency,
    # including any malicious parts, is treated as a single, literal argument.
    fixed_cmd_list = [pip_executable_path, "install", "--dry-run"] + dependencies

    print(f"Executing securely with the following command list:\n{fixed_cmd_list}\n")

    try:
        # By default, `shell=False`. The operating system executes the command
        # directly, passing the items from `fixed_cmd_list` as arguments.
        # No shell interpretation of the arguments occurs.
        # We use `--dry-run` to avoid actually installing packages.
        result = subprocess.run(
            fixed_cmd_list,
            capture_output=True,
            text=True,
            check=False,  # We set to False to inspect the output on failure
        )

        print("--- Subprocess STDOUT ---")
        # In a dry run, pip lists what it would do.
        print(result.stdout if result.stdout else "[No stdout]")
        print("\n--- Subprocess STDERR ---")
        print(result.stderr if result.stderr else "[No stderr]")
        print("-------------------------")

        if result.returncode == 0:
            print("\n✅ SUCCESS: Pip command executed successfully (dry run).")
        else:
            print(f"\n❌ FAILURE: Pip command failed with return code {result.returncode}.")
            print(
                "\nThis is the expected and SAFE outcome. The malicious command was not "
                "executed. Instead, pip treated 'scikit-learn;' as part of a package name "
                "and failed to find a matching distribution, preventing the attack."
            )

    except FileNotFoundError:
        print("Error: 'pip' command not found. Please ensure Python is installed and in your PATH.")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")


if __name__ == "__main__":
    print("--- Demonstration of Fix for CVE-2025-15379 ---")
    print(
        "This script shows how the patched MLflow code handles a malicious dependency string.\n"
    )

    # A malicious command to be injected.
    # On Linux/macOS, this tries to create a file. On Windows, it runs 'echo'.
    injected_command = "touch /tmp/pwned_by_cve" if sys.platform != "win32" else "echo pwned"

    # Simulate a malicious `python_env.yaml` where an attacker has injected a command.
    malicious_dependencies = [
        "scikit-learn",
        f"pandas; {injected_command}",  # The malicious payload
    ]

    print(f"Simulating installation with malicious dependencies:\n{malicious_dependencies}\n")

    # Run the fixed function with the malicious input.
    _install_model_dependencies_to_env(malicious_dependencies)

    # Verify that the command injection was NOT successful.
    if sys.platform != "win32":
        if not os.path.exists("/tmp/pwned_by_cve"):
            print("\nVerification successful: The malicious file '/tmp/pwned_by_cve' was NOT created.")
        else:
            print("\nVerification FAILED: The malicious file WAS created.")
            # Clean up the created file if the check fails for any reason
            os.remove("/tmp/pwned_by_cve")

Payload

channels:
  - defaults
dependencies:
  - python=3.8.13
  - pip:
    - "scikit-learn; touch /tmp/pwned"
name: mlflow-env

Cite this entry

@misc{vaitp:cve202515379,
  title        = {{Command injection in MLflow via unsanitized dependencies in model artifacts.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-15379},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-15379/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::