high

CVE

CVE-2026-118613, CVE-2026-118614, CVE-2026-118615, CVE-2026-118616, CVE-2026-118617, CVE-2026-118618, CVE-2026-118619, CVE-2026-118683, CVE-2026-118684

CWE

CWE-502, CWE-94, CWE-78, CWE-22, CWE-327

Affected Surface

  • PyPI `ultralytics` (`ultralytics/ultralytics`): ZDI advisories name `8.4.65` (`CVE-2026-118613` through `CVE-2026-118618`) and `8.4.103` (`CVE-2026-118619`); default `weights_only=False` checkpoint loading, dataset YAML `download:` execution, `np.load(..., allow_pickle=True)` cache loading, and unvalidated Google Drive `Content-Disposition` filenames remain present through `8.4.192` unless `ULTRALYTICS_SAFE_LOAD=1` is set and untrusted datasets/links are blocked
  • PyPI `monai` (`Project-MONAI/MONAI`) versions `>= 1.2.0, < 1.6.1` (`CVE-2026-118683` / `GHSA-wvpx-5qmp-46g3`) and `< 1.6.1` (`CVE-2026-118684` / `GHSA-hhh4-h52m-fqh6`)
  • Computer-vision and medical-imaging training, evaluation, and inference pipelines that load third-party `.pt` checkpoints, dataset `.yaml` descriptors, `.cache` files, Google Drive share links, or MONAI Model Zoo bundles

TL;DR

On 28 October 2026, nine vulnerabilities were disclosed across two widely deployed Python computer-vision and medical-imaging frameworks, Ultralytics YOLO (ultralytics on PyPI) and Project MONAI (monai on PyPI):

  1. Seven ZDI (0Day) RCE advisories in Ultralytics (ZDI-26-812 through ZDI-26-818 / CVE-2026-118613 through CVE-2026-118619, CVSS 7.8 to 8.8 High): Trend Micro’s Zero Day Initiative published seven advisories after reporting six of them to Ultralytics’s GitHub account on 18 June 2026 and the seventh on 26 August 2026. Although ZDI’s advisories list No fix available, inspecting the ultralytics/ultralytics repository history shows a split reality:
    • Fixed in >= 8.4.116 (CVE-2026-118615): When torch_safe_load() caught ModuleNotFoundError during unpickling, it passed the pickle-supplied module name e.name into check_requirements(e.name), which ran subprocess.check_output(..., shell=True). Commit 16d2ae072e (PR #25720, 9 August 2026) removed shell=True and passed packages as an argv list.
    • Gated behind opt-in ULTRALYTICS_SAFE_LOAD=1 (CVE-2026-118613, CVE-2026-118616, CVE-2026-118618): Starting in 8.4.67 (commit 5ed793530f, PR #24829), Ultralytics added _SafeLoad, which uses torch.load(..., weights_only=True) with a strict add_safe_globals allowlist, parses activation: YAML fields via AST instead of eval(act), and restricts parse_model() layer classes to torch.nn.Module subclasses. However, ULTRALYTICS_SAFE_LOAD is False by default through 8.4.192. Unless you set ULTRALYTICS_SAFE_LOAD=1 (or pass safe_only=True), torch_load() forces weights_only=False and parse_model() runs eval(act) and globals()[m](*args).
    • Still active by default in 8.4.192 (CVE-2026-118614, CVE-2026-118617, CVE-2026-118619): check_det_dataset() still executes the download: field of a dataset .yaml file via exec(s, {"yaml": data}) or subprocess.run(s.split()) (CVE-2026-118614); load_dataset_cache_file() still calls np.load(str(path), allow_pickle=True).item() on .cache files (CVE-2026-118617); and get_google_drive_file_info() still extracts the raw filename="..." string from a Google Drive response’s Content-Disposition header and joins it directly onto the destination path (Path(dir or ".") / file) without stripping ../ segments (CVE-2026-118619, CVSS 8.8).
  2. Project MONAI bundle configs/logging.conf RCE (CVE-2026-118683 / GHSA-wvpx-5qmp-46g3, CVSS 8.8 High) and check_hash bypass (CVE-2026-118684 / GHSA-hhh4-h52m-fqh6, CVSS v4.0 8.2 High / v3.1 5.9 Medium), fixed in monai 1.6.1: When initializing a MONAI bundle (ConfigWorkflow.__init__), MONAI automatically loaded the bundle’s configs/logging.conf file with Python’s logging.config.fileConfig() before parsing or validating any bundle config. Because standard-library fileConfig() calls eval() on every handler’s class= and args= INI entries, an untrusted MONAI bundle achieved immediate code execution on load. MONAI 1.6.1 (commit db6f171bad) parses logging.conf with ast.parse first and restricts class= to logging / logging.handlers classes and args= / kwargs= to literals plus sys.stdout / sys.stderr.

What to do now: Upgrade monai to >= 1.6.1. For ultralytics, upgrade to >= 8.4.192 (so check_requirements no longer uses shell=True), export ULTRALYTICS_SAFE_LOAD=1 and YOLO_AUTOINSTALL=false in every training and inference environment, and never pass untrusted dataset .yaml files, .cache files, or external Google Drive URLs into Ultralytics.

Affected packages and advisories

EcosystemPackageCVE / AdvisorySeverityNamed / affected versionsFixed / mitigation statusVulnerable function & file
PyPIultralyticsCVE-2026-118619 (ZDI-26-818)CVSS 8.8 HighNamed on 8.4.103; present through 8.4.192Unpatched (do not pass untrusted Google Drive URLs to safe_download or pass explicit file=)get_google_drive_file_info() and safe_download() in ultralytics/utils/downloads.py
PyPIultralyticsCVE-2026-118613 (ZDI-26-812)CVSS 7.8 HighNamed on 8.4.65; default behavior through 8.4.192Opt-in mitigation in >= 8.4.67: set ULTRALYTICS_SAFE_LOAD=1torch_load() in ultralytics/utils/patches.py (weights_only=False default)
PyPIultralyticsCVE-2026-118615 (ZDI-26-814)CVSS 7.8 High<= 8.4.115 (named on 8.4.65)Fixed in >= 8.4.116 (commit 16d2ae072e); also set YOLO_AUTOINSTALL=false and ULTRALYTICS_SAFE_LOAD=1torch_safe_load() in ultralytics/nn/tasks.py -> check_requirements(e.name) in ultralytics/utils/checks.py
PyPIultralyticsCVE-2026-118616 (ZDI-26-815), CVE-2026-118618 (ZDI-26-817)CVSS 7.8 HighNamed on 8.4.65; default behavior through 8.4.192Opt-in mitigation in >= 8.4.67: set ULTRALYTICS_SAFE_LOAD=1 (activates _SafeLoad.activation() and issubclass(m, nn.Module) check)parse_model() in ultralytics/nn/tasks.py (eval(act) and globals()[m])
PyPIultralyticsCVE-2026-118614 (ZDI-26-813)CVSS 7.8 HighNamed on 8.4.65; present through 8.4.192Unpatched by design (pass autodownload=False and never load untrusted dataset .yaml files)check_det_dataset() in ultralytics/data/utils.py (exec(s, {"yaml": data}) and subprocess.run(s.split()))
PyPIultralyticsCVE-2026-118617 (ZDI-26-816)CVSS 7.8 HighNamed on 8.4.65; present through 8.4.192Unpatched (remove untrusted *.cache files from dataset directories before training/val)load_dataset_cache_file() in ultralytics/data/utils.py (np.load(str(path), allow_pickle=True))
PyPImonaiCVE-2026-118683 (GHSA-wvpx-5qmp-46g3)CVSS 8.8 High>= 1.2.0, < 1.6.11.6.1 (commit db6f171bad)ConfigWorkflow.__init__() in monai/bundle/workflows.py
PyPImonaiCVE-2026-118684 (GHSA-hhh4-h52m-fqh6)CVSS v4.0 8.2 High (v3.1 5.9 Medium)< 1.6.11.6.1 (commit aff7aad661 / 542ef8e424)check_hash() and download_url() in monai/apps/utils.py

Technical analysis

1. Ultralytics checkpoint loading and model parsing (CVE-2026-118613, CVE-2026-118615, CVE-2026-118616, and CVE-2026-118618)

PyTorch 2.6 changed torch.load() to default to weights_only=True so that loading a checkpoint does not execute arbitrary Python pickle opcodes. In ultralytics/utils/patches.py, Ultralytics wraps torch.load with torch_load(), which explicitly sets weights_only=False whenever the caller does not pass weights_only (CVE-2026-118613 / ZDI-26-812):

# ultralytics/utils/patches.py
def torch_load(*args, **kwargs):
    from ultralytics.utils.torch_utils import TORCH_1_13

    if TORCH_1_13 and "weights_only" not in kwargs:
        kwargs["weights_only"] = False

    return torch.load(*args, **kwargs)

Worse, in ultralytics <= 8.4.115, torch_safe_load() in ultralytics/nn/tasks.py caught ModuleNotFoundError as e during torch_load() and handed the missing module name e.name (a string controlled directly by the pickle stream) to check_requirements(e.name). Inside ultralytics/utils/checks.py, check_requirements() stripped the prefix up to the last / (r_stripped = candidate.rpartition("/")[-1].replace(".git", "")) to test re.match(r"([a-zA-Z0-9-_]+)...", r_stripped), but then passed the full unstripped string candidate into subprocess.check_output(..., shell=True) (CVE-2026-118615 / ZDI-26-814):

--- a/ultralytics/utils/checks.py
+++ b/ultralytics/utils/checks.py
@@ -598,17 +599,23 @@ def attempt_install(packages, commands, use_uv):
             return subprocess.check_output(
-                f'uv pip install --no-cache-dir --python "{sys.executable}" {packages} {commands} '
-                f"--index-strategy=unsafe-best-match --break-system-packages",
-                shell=True,
+                [
+                    "uv",
+                    "pip",
+                    "install",
+                    "--no-cache-dir",
+                    "--python",
+                    sys.executable,
+                    *packages,
+                    *shlex.split(commands),
+                    "--index-strategy=unsafe-best-match",
+                    "--break-system-packages",
+                ],
                 stderr=subprocess.STDOUT,
                 text=True,
             )
         return subprocess.check_output(
-            f'"{sys.executable}" -m pip install --no-cache-dir {packages} {commands}',
-            shell=True,
+            [sys.executable, "-m", "pip", "install", "--no-cache-dir", *packages, *shlex.split(commands)],
             stderr=subprocess.STDOUT,
             text=True,
         )

As the regression test in commit 16d2ae072e (#25720) demonstrates, a pickle referencing a module named "$(touch /tmp/pwned)/missing" passed the rpartition("/")[-1] regex check ("missing") and executed touch /tmp/pwned via /bin/sh when attempt_install() ran.

Meanwhile, parse_model(d, ch) in ultralytics/nn/tasks.py constructs a model from a YAML dictionary d (either loaded from a .yaml file or embedded inside a checkpoint). In 8.4.65 (and by default today when ULTRALYTICS_SAFE_LOAD is not enabled), parse_model() contained two code-injection sinks (CVE-2026-118616 / ZDI-26-815 and CVE-2026-118618 / ZDI-26-817):

  1. Unrestricted eval(act) on the top-level activation: key:
    # ultralytics/nn/tasks.py
    restricted = _SafeLoad.restricted()
    if act:
        Conv.default_act = _SafeLoad.activation(act) if restricted else eval(act)
  2. Arbitrary symbol lookup and instantiation in d["backbone"] + d["head"]: Each layer entry [f, n, m, args] resolves m via getattr(torch.nn, m[3:]), getattr(__import__("torchvision").ops, m[16:]), or globals()[m], and then calls m(*args) at the end of the loop. When restricted is False, any callable present in tasks.py’s global namespace (such as torch_safe_load, check_requirements, check_yaml, or Path) can be invoked with attacker-controlled args.

Setting ULTRALYTICS_SAFE_LOAD=1 (>= 8.4.67, commit 5ed793530f) enables _SafeLoad, which:

  • Loads checkpoints with torch_load(file, map_location="cpu", weights_only=True) against an allowlist of torch.nn.Module subclasses (_SafeLoad.loading()).
  • Refuses to auto-install missing modules on ModuleNotFoundError.
  • Parses activation via _SafeLoad.activation(act) using ast.parse and ast.literal_eval, allowing only [torch.]nn.<Module>(literal_args).
  • Enforces isinstance(m, type) and issubclass(m, torch.nn.Module) for every layer in parse_model().

2. Ultralytics dataset YAML exec(), .cache unpickling, and Google Drive path traversal (CVE-2026-118614, CVE-2026-118617, and CVE-2026-118619)

Three of the seven ZDI advisories affect dataset and download helpers that remain active even when ULTRALYTICS_SAFE_LOAD=1 is set:

  • CVE-2026-118614 (ZDI-26-813, check_det_dataset in ultralytics/data/utils.py): When a dataset .yaml file specifies a train or val path that does not exist locally and includes a download: string s, check_det_dataset() executes s directly as a bash command or Python script:
    # ultralytics/data/utils.py lines 820-828
    if s.lower().startswith("http") and clean_url(s).lower().endswith(
        (".zip", ".tar", ".gz", ".tgz", ".xz", ".bz2", ".txz", ".tbz2")
    ):  # URL
        safe_download(url=s, dir=DATASETS_DIR, delete=True)
    elif s.startswith("bash "):  # bash script
        LOGGER.info(f"Running {s} ...")
        subprocess.run(s.split(), check=True)
    else:  # python script
        exec(s, {"yaml": data})  # noqa: S102
  • CVE-2026-118617 (ZDI-26-816, load_dataset_cache_file in ultralytics/data/utils.py): When scanning a dataset directory, Ultralytics looks for a precomputed *.cache file and loads it with np.load(str(path), allow_pickle=True).item(). Because NumPy’s allow_pickle=True invokes Python’s pickle module, opening a third-party dataset archive that bundles a crafted .cache file executes arbitrary code immediately.
  • CVE-2026-118619 (ZDI-26-818, CVSS 8.8 High, Google Drive download path traversal in ultralytics/utils/downloads.py): When safe_download(url, dir=...) is called with a https://drive.google.com/... URL and file=None, it calls get_google_drive_file_info(url), which extracts the filename from the HTTP response’s Content-Disposition header using re.findall('filename="(.+)"', cd)[0] and joins it directly onto dir:
    # ultralytics/utils/downloads.py lines 308-310 and 365-374
    if cd := response.headers.get("content-disposition"):
        filename = re.findall('filename="(.+)"', cd)[0]
    ...
    if gdrive:
        url, file = get_google_drive_file_info(url)
    url = url.replace(" ", "%20")
    
    f = Path(dir or ".") / (file or url2file(url))  # URL converted to filename
    if not f.is_file():
        ...
        f.parent.mkdir(parents=True, exist_ok=True)
    Because url2file() strips path components via Path(...).name on ordinary URLs, but get_google_drive_file_info() returns the raw Content-Disposition filename without calling Path(filename).name, a Google Drive file whose name contains ../../... (for example ../../.bashrc or ../../.ssh/authorized_keys) traverses out of dir and writes the downloaded payload to an arbitrary path.

3. Project MONAI 1.6.1: configs/logging.conf eval() RCE (CVE-2026-118683) and check_hash hardening (CVE-2026-118684)

In Project MONAI (>= 1.2.0, < 1.6.1), ConfigWorkflow.__init__() in monai/bundle/workflows.py automatically checks whether a bundle directory contains configs/logging.conf and, if present, passes that file to logging.config.fileConfig() in the constructor, before any bundle JSON/YAML configuration is inspected.

In Python’s standard library (logging/config.py), fileConfig() resolves each [handler_*] section’s class= option and args= / kwargs= options by passing them directly to Python’s built-in eval() inside a namespace containing logging, os, and sys. An attacker who published a MONAI bundle with a malicious configs/logging.conf (for example setting class=os.system and args=('id',) or args=(__import__('os').system('...'),)) executed arbitrary code as soon as a user instantiated ConfigWorkflow or ran python -m monai.bundle run.

In commit db6f171bad (monai 1.6.1), MONAI adds _reject_executable_logging_config(), which reads logging.conf into memory once, parses every class, args, and kwargs option with ast.parse(..., mode="eval"), verifies that class= resolves to an actual logging.Handler or logging.Formatter subclass inside logging or logging.handlers, requires args= / kwargs= to contain only AST literals (Constant, Tuple, List, Dict, Set) or sys.stdout / sys.stderr, and then feeds the validated string buffer (io.StringIO(content)) to fileConfig() to prevent a TOCTOU file swap:

--- a/monai/bundle/workflows.py
+++ b/monai/bundle/workflows.py
@@ -34,6 +38,31 @@ logger = get_logger(module_name=__name__)
+# `class=` values accepted in a bundle's logging INI. `fileConfig` eval()s this field, so the
+# allowlist is by module: stdlib logging handlers/formatters cover every legitimate bundle.
+_ALLOWED_LOGGING_CLASS_MODULES = {"logging", "logging.handlers"}
+
+def _reject_executable_logging_config(logging_file: str) -> str:
...
+    for section in parser.sections():
+        for field in ("class", "args", "kwargs"):
+            if not parser.has_option(section, field):
+                continue
+            value = parser.get(section, field, raw=True).strip()
+            if not value:
+                continue
+            if field == "class":
+                _reject_non_logging_class(value, logging_file, section)
+            else:
+                _reject_non_literal_expression(value, logging_file, section, field)
+    return content

Alongside CVE-2026-118683, MONAI 1.6.1 also addresses CVE-2026-118684 (GHSA-hhh4-h52m-fqh6, commit aff7aad661 and 542ef8e424): check_hash() and download_url() in monai/apps/utils.py switched their default hash_type from "md5" to "sha256", emit a UserWarning whenever val=None leaves a downloaded file unverified, and pair with weights_only=True enforcement across bundle checkpoint loaders.

Detection and remediation

# 1. Check installed versions of ultralytics and monai across virtualenvs and lockfiles
pip show ultralytics monai 2>/dev/null || true
rg -n '(ultralytics|monai)' requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock 2>/dev/null

# 2. Audit code and configs for Ultralytics Google Drive downloads, dataset YAMLs with download: blocks, or .cache files
rg -n 'drive\.google\.com|torch_safe_load|torch_load|check_det_dataset|load_dataset_cache_file' --glob '*.{py,yaml,yml}' . 2>/dev/null
find . -name '*.cache' -ls 2>/dev/null

# 3. Audit MONAI bundles for non-standard class= or args= entries in configs/logging.conf
rg -n '^\s*(class|args|kwargs)\s*=' --glob '**/logging.conf' . 2>/dev/null
  1. Upgrade monai to >= 1.6.1:
    pip install --upgrade "monai>=1.6.1"
    When downloading MONAI bundles or datasets via download_url() or download_and_extract(), always supply an explicit SHA-256 hash_val.
  2. Harden ultralytics deployments immediately:
    • Upgrade to ultralytics >= 8.4.192 so check_requirements() no longer passes requirement strings to shell=True (CVE-2026-118615).
    • Export ULTRALYTICS_SAFE_LOAD=1 and YOLO_AUTOINSTALL=false in Dockerfiles, systemd units, and CI jobs so checkpoint loads enforce weights_only=True and parse_model() disables eval(act) and non-nn.Module globals (CVE-2026-118613, CVE-2026-118616, CVE-2026-118618).
    • Before loading any third-party dataset, delete any bundled *.cache files (CVE-2026-118617), inspect the dataset .yaml to confirm it does not define a download: script (CVE-2026-118614), and never pass untrusted https://drive.google.com/... URLs into safe_download() (CVE-2026-118619).

Timeline

  • 2026-06-14: Ultralytics merges PR #24829 (5ed793530f) in ultralytics 8.4.67, introducing opt-in ULTRALYTICS_SAFE_LOAD (_SafeLoad).
  • 2026-06-18: ZDI reports ZDI-CAN-29652, 30119, 30121, 30122, 30123, and 30124 (CVE-2026-118613 through CVE-2026-118618) to Ultralytics’s GitHub account.
  • 2026-08-09: Ultralytics merges PR #25720 (16d2ae072e), removing shell=True from check_requirements().
  • 2026-08-26: ZDI reports ZDI-CAN-32376 (CVE-2026-118619, Google Drive download path traversal) to Ultralytics.
  • 2026-09-03 to 2026-09-17: Project MONAI merges PR #9088 (aff7aad661, check_hash SHA-256 default) and PR #9114 (db6f171bad, _reject_executable_logging_config for GHSA-wvpx-5qmp-46g3), shipping in monai 1.6.1.
  • 2026-10-22: After asking for updates on 19 October, ZDI notifies Ultralytics of its intent to publish ZDI-26-812 through ZDI-26-818 as zero-day advisories.
  • 2026-10-28: ZDI and NVD publish CVE-2026-118613 through CVE-2026-118619 (ZDI-26-812 through ZDI-26-818), and NVD publishes CVE-2026-118683 and CVE-2026-118684 for Project MONAI.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References