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.
- CWE
- CWE-77
- 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 ::
