VYPR
Critical severity9.8NVD Advisory· Published Mar 30, 2026· Updated Apr 28, 2026

CVE-2025-15379

CVE-2025-15379

Description

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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mlflowPyPI
< 3.8.13.8.1

Affected products

1
  • cpe:2.3:a:lfprojects:mlflow:*:*:*:*:*:*:*:*
    Range: >=3.8.0,<=3.8.1

Patches

2
a22ce7157f64

fix(security): prevent command injection via malicious model artifacts (#19583)

https://github.com/mlflow/mlflowCole MurrayDec 24, 2025via ghsa
2 files changed · +244 4
  • mlflow/models/container/__init__.py+11 4 modified
    @@ -8,6 +8,7 @@
     import logging
     import multiprocessing
     import os
    +import shlex
     import shutil
     import signal
     import sys
    @@ -138,12 +139,18 @@ def _install_model_dependencies_to_env(model_path, env_manager) -> list[str]:
         env_conf = conf[mlflow.pyfunc.ENV]
     
         if env_manager == em.LOCAL:
    -        # Install pip dependencies directly into the local environment
             python_env_config_path = os.path.join(model_path, env_conf[em.VIRTUALENV])
             python_env = _PythonEnv.from_yaml(python_env_config_path)
    -        deps = " ".join(python_env.build_dependencies + python_env.dependencies)
    -        deps = deps.replace("requirements.txt", os.path.join(model_path, "requirements.txt"))
    -        if Popen(["bash", "-c", f"python -m pip install {deps}"]).wait() != 0:
    +
    +        pip_args = [sys.executable, "-m", "pip", "install"]
    +        for dep in python_env.build_dependencies + python_env.dependencies:
    +            dep_args = shlex.split(dep)
    +            for i, arg in enumerate(dep_args):
    +                if arg == "requirements.txt" or arg.endswith("/requirements.txt"):
    +                    dep_args[i] = os.path.join(model_path, "requirements.txt")
    +            pip_args.extend(dep_args)
    +
    +        if Popen(pip_args).wait() != 0:
                 raise Exception("Failed to install model dependencies.")
             return []
     
    
  • tests/models/test_container.py+233 0 added
    @@ -0,0 +1,233 @@
    +"""
    +Tests for mlflow.models.container module.
    +
    +Includes security tests for command injection prevention.
    +"""
    +
    +import os
    +from unittest import mock
    +
    +import pytest
    +import yaml
    +
    +from mlflow.models.container import _install_model_dependencies_to_env
    +from mlflow.utils import env_manager as em
    +
    +
    +def _create_model_artifact(model_path, dependencies, build_dependencies=None):
    +    """Helper to create a minimal model artifact for testing."""
    +    with open(os.path.join(model_path, "MLmodel"), "w") as f:
    +        yaml.dump(
    +            {
    +                "flavors": {
    +                    "python_function": {
    +                        "env": {"virtualenv": "python_env.yaml"},
    +                        "loader_module": "mlflow.pyfunc.model",
    +                    }
    +                }
    +            },
    +            f,
    +        )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("")
    +
    +    with open(os.path.join(model_path, "python_env.yaml"), "w") as f:
    +        yaml.dump(
    +            {
    +                "python": "3.12",
    +                "build_dependencies": build_dependencies or [],
    +                "dependencies": dependencies,
    +            },
    +            f,
    +        )
    +
    +
    +def test_command_injection_via_semicolon_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy; echo INJECTED > /tmp/test_injection_semicolon.txt; #"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_semicolon.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via semicolon succeeded!"
    +
    +
    +def test_command_injection_via_pipe_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy | echo INJECTED > /tmp/test_injection_pipe.txt"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_pipe.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via pipe succeeded!"
    +
    +
    +def test_command_injection_via_backticks_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["`echo INJECTED > /tmp/test_injection_backtick.txt`"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_backtick.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via backticks succeeded!"
    +
    +
    +def test_command_injection_via_dollar_parens_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["$(echo INJECTED > /tmp/test_injection_dollar.txt)"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_dollar.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via $() succeeded!"
    +
    +
    +def test_command_injection_via_ampersand_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy && echo INJECTED > /tmp/test_injection_ampersand.txt"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_ampersand.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via && succeeded!"
    +
    +
    +def test_legitimate_package_install(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +        build_dependencies=[],
    +    )
    +
    +    result = _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +    assert result == []
    +
    +
    +def test_requirements_file_reference(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["-r requirements.txt"],
    +        build_dependencies=["pip"],
    +    )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("# empty requirements\n")
    +
    +    result = _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +    assert result == []
    +
    +
    +def test_requirements_path_replacement(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["-r requirements.txt"],
    +    )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("six\n")
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert isinstance(call_args, list), "Should use list args, not shell string"
    +
    +        assert "-r" in call_args
    +        req_index = call_args.index("-r")
    +        req_path = call_args[req_index + 1]
    +        assert req_path == os.path.join(model_path, "requirements.txt")
    +
    +
    +def test_no_shell_execution(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args
    +        assert isinstance(call_args[0][0], list)
    +        assert call_args[1].get("shell") is not True
    +
    +
    +def test_build_dependencies_processed(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +        build_dependencies=["setuptools", "wheel"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert "setuptools" in call_args
    +        assert "wheel" in call_args
    +        assert "pip" in call_args
    +
    +
    +def test_package_name_with_requirements_substring_not_modified(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["my-requirements.txt-parser", "requirements.txt-tools"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert "my-requirements.txt-parser" in call_args
    +        assert "requirements.txt-tools" in call_args
    +        assert not any(model_path in arg for arg in call_args if "parser" in arg or "tools" in arg)
    
361b6f620adf

fix(security): prevent command injection via malicious model artifacts (#19583)

https://github.com/mlflow/mlflowCole MurrayDec 24, 2025via ghsa
2 files changed · +244 4
  • mlflow/models/container/__init__.py+11 4 modified
    @@ -8,6 +8,7 @@
     import logging
     import multiprocessing
     import os
    +import shlex
     import shutil
     import signal
     import sys
    @@ -138,12 +139,18 @@ def _install_model_dependencies_to_env(model_path, env_manager) -> list[str]:
         env_conf = conf[mlflow.pyfunc.ENV]
     
         if env_manager == em.LOCAL:
    -        # Install pip dependencies directly into the local environment
             python_env_config_path = os.path.join(model_path, env_conf[em.VIRTUALENV])
             python_env = _PythonEnv.from_yaml(python_env_config_path)
    -        deps = " ".join(python_env.build_dependencies + python_env.dependencies)
    -        deps = deps.replace("requirements.txt", os.path.join(model_path, "requirements.txt"))
    -        if Popen(["bash", "-c", f"python -m pip install {deps}"]).wait() != 0:
    +
    +        pip_args = [sys.executable, "-m", "pip", "install"]
    +        for dep in python_env.build_dependencies + python_env.dependencies:
    +            dep_args = shlex.split(dep)
    +            for i, arg in enumerate(dep_args):
    +                if arg == "requirements.txt" or arg.endswith("/requirements.txt"):
    +                    dep_args[i] = os.path.join(model_path, "requirements.txt")
    +            pip_args.extend(dep_args)
    +
    +        if Popen(pip_args).wait() != 0:
                 raise Exception("Failed to install model dependencies.")
             return []
     
    
  • tests/models/test_container.py+233 0 added
    @@ -0,0 +1,233 @@
    +"""
    +Tests for mlflow.models.container module.
    +
    +Includes security tests for command injection prevention.
    +"""
    +
    +import os
    +from unittest import mock
    +
    +import pytest
    +import yaml
    +
    +from mlflow.models.container import _install_model_dependencies_to_env
    +from mlflow.utils import env_manager as em
    +
    +
    +def _create_model_artifact(model_path, dependencies, build_dependencies=None):
    +    """Helper to create a minimal model artifact for testing."""
    +    with open(os.path.join(model_path, "MLmodel"), "w") as f:
    +        yaml.dump(
    +            {
    +                "flavors": {
    +                    "python_function": {
    +                        "env": {"virtualenv": "python_env.yaml"},
    +                        "loader_module": "mlflow.pyfunc.model",
    +                    }
    +                }
    +            },
    +            f,
    +        )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("")
    +
    +    with open(os.path.join(model_path, "python_env.yaml"), "w") as f:
    +        yaml.dump(
    +            {
    +                "python": "3.12",
    +                "build_dependencies": build_dependencies or [],
    +                "dependencies": dependencies,
    +            },
    +            f,
    +        )
    +
    +
    +def test_command_injection_via_semicolon_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy; echo INJECTED > /tmp/test_injection_semicolon.txt; #"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_semicolon.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via semicolon succeeded!"
    +
    +
    +def test_command_injection_via_pipe_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy | echo INJECTED > /tmp/test_injection_pipe.txt"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_pipe.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via pipe succeeded!"
    +
    +
    +def test_command_injection_via_backticks_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["`echo INJECTED > /tmp/test_injection_backtick.txt`"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_backtick.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via backticks succeeded!"
    +
    +
    +def test_command_injection_via_dollar_parens_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["$(echo INJECTED > /tmp/test_injection_dollar.txt)"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_dollar.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via $() succeeded!"
    +
    +
    +def test_command_injection_via_ampersand_blocked(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["numpy && echo INJECTED > /tmp/test_injection_ampersand.txt"],
    +    )
    +
    +    evidence_file = "/tmp/test_injection_ampersand.txt"
    +    if os.path.exists(evidence_file):
    +        os.remove(evidence_file)
    +
    +    with pytest.raises(Exception, match="Failed to install model dependencies"):
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +    assert not os.path.exists(evidence_file), "Command injection via && succeeded!"
    +
    +
    +def test_legitimate_package_install(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +        build_dependencies=[],
    +    )
    +
    +    result = _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +    assert result == []
    +
    +
    +def test_requirements_file_reference(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["-r requirements.txt"],
    +        build_dependencies=["pip"],
    +    )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("# empty requirements\n")
    +
    +    result = _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +    assert result == []
    +
    +
    +def test_requirements_path_replacement(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["-r requirements.txt"],
    +    )
    +
    +    with open(os.path.join(model_path, "requirements.txt"), "w") as f:
    +        f.write("six\n")
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert isinstance(call_args, list), "Should use list args, not shell string"
    +
    +        assert "-r" in call_args
    +        req_index = call_args.index("-r")
    +        req_path = call_args[req_index + 1]
    +        assert req_path == os.path.join(model_path, "requirements.txt")
    +
    +
    +def test_no_shell_execution(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args
    +        assert isinstance(call_args[0][0], list)
    +        assert call_args[1].get("shell") is not True
    +
    +
    +def test_build_dependencies_processed(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["pip"],
    +        build_dependencies=["setuptools", "wheel"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert "setuptools" in call_args
    +        assert "wheel" in call_args
    +        assert "pip" in call_args
    +
    +
    +def test_package_name_with_requirements_substring_not_modified(tmp_path):
    +    model_path = str(tmp_path)
    +    _create_model_artifact(
    +        model_path,
    +        dependencies=["my-requirements.txt-parser", "requirements.txt-tools"],
    +    )
    +
    +    with mock.patch("mlflow.models.container.Popen") as mock_popen:
    +        mock_popen.return_value.wait.return_value = 0
    +
    +        _install_model_dependencies_to_env(model_path, env_manager=em.LOCAL)
    +
    +        call_args = mock_popen.call_args[0][0]
    +        assert "my-requirements.txt-parser" in call_args
    +        assert "requirements.txt-tools" in call_args
    +        assert not any(model_path in arg for arg in call_args if "parser" in arg or "tools" in arg)
    

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

5

News mentions

0

No linked articles in our index yet.