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):
- Seven ZDI
(0Day)RCE advisories in Ultralytics (ZDI-26-812throughZDI-26-818/CVE-2026-118613throughCVE-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 listNo fix available, inspecting theultralytics/ultralyticsrepository history shows a split reality:- Fixed in
>= 8.4.116(CVE-2026-118615): Whentorch_safe_load()caughtModuleNotFoundErrorduring unpickling, it passed the pickle-supplied module namee.nameintocheck_requirements(e.name), which ransubprocess.check_output(..., shell=True). Commit16d2ae072e(PR#25720, 9 August 2026) removedshell=Trueand passed packages as an argv list. - Gated behind opt-in
ULTRALYTICS_SAFE_LOAD=1(CVE-2026-118613,CVE-2026-118616,CVE-2026-118618): Starting in8.4.67(commit5ed793530f, PR#24829), Ultralytics added_SafeLoad, which usestorch.load(..., weights_only=True)with a strictadd_safe_globalsallowlist, parsesactivation:YAML fields via AST instead ofeval(act), and restrictsparse_model()layer classes totorch.nn.Modulesubclasses. However,ULTRALYTICS_SAFE_LOADisFalseby default through8.4.192. Unless you setULTRALYTICS_SAFE_LOAD=1(or passsafe_only=True),torch_load()forcesweights_only=Falseandparse_model()runseval(act)andglobals()[m](*args). - Still active by default in
8.4.192(CVE-2026-118614,CVE-2026-118617,CVE-2026-118619):check_det_dataset()still executes thedownload:field of a dataset.yamlfile viaexec(s, {"yaml": data})orsubprocess.run(s.split())(CVE-2026-118614);load_dataset_cache_file()still callsnp.load(str(path), allow_pickle=True).item()on.cachefiles (CVE-2026-118617); andget_google_drive_file_info()still extracts the rawfilename="..."string from a Google Drive response’sContent-Dispositionheader and joins it directly onto the destination path (Path(dir or ".") / file) without stripping../segments (CVE-2026-118619, CVSS 8.8).
- Fixed in
- Project MONAI bundle
configs/logging.confRCE (CVE-2026-118683/GHSA-wvpx-5qmp-46g3, CVSS 8.8 High) andcheck_hashbypass (CVE-2026-118684/GHSA-hhh4-h52m-fqh6, CVSS v4.0 8.2 High / v3.1 5.9 Medium), fixed inmonai 1.6.1: When initializing a MONAI bundle (ConfigWorkflow.__init__), MONAI automatically loaded the bundle’sconfigs/logging.conffile with Python’slogging.config.fileConfig()before parsing or validating any bundle config. Because standard-libraryfileConfig()callseval()on every handler’sclass=andargs=INI entries, an untrusted MONAI bundle achieved immediate code execution on load. MONAI1.6.1(commitdb6f171bad) parseslogging.confwithast.parsefirst and restrictsclass=tologging/logging.handlersclasses andargs=/kwargs=to literals plussys.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
| Ecosystem | Package | CVE / Advisory | Severity | Named / affected versions | Fixed / mitigation status | Vulnerable function & file |
|---|---|---|---|---|---|---|
| PyPI | ultralytics | CVE-2026-118619 (ZDI-26-818) | CVSS 8.8 High | Named on 8.4.103; present through 8.4.192 | Unpatched (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 |
| PyPI | ultralytics | CVE-2026-118613 (ZDI-26-812) | CVSS 7.8 High | Named on 8.4.65; default behavior through 8.4.192 | Opt-in mitigation in >= 8.4.67: set ULTRALYTICS_SAFE_LOAD=1 | torch_load() in ultralytics/utils/patches.py (weights_only=False default) |
| PyPI | ultralytics | CVE-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=1 | torch_safe_load() in ultralytics/nn/tasks.py -> check_requirements(e.name) in ultralytics/utils/checks.py |
| PyPI | ultralytics | CVE-2026-118616 (ZDI-26-815), CVE-2026-118618 (ZDI-26-817) | CVSS 7.8 High | Named on 8.4.65; default behavior through 8.4.192 | Opt-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]) |
| PyPI | ultralytics | CVE-2026-118614 (ZDI-26-813) | CVSS 7.8 High | Named on 8.4.65; present through 8.4.192 | Unpatched 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())) |
| PyPI | ultralytics | CVE-2026-118617 (ZDI-26-816) | CVSS 7.8 High | Named on 8.4.65; present through 8.4.192 | Unpatched (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)) |
| PyPI | monai | CVE-2026-118683 (GHSA-wvpx-5qmp-46g3) | CVSS 8.8 High | >= 1.2.0, < 1.6.1 | 1.6.1 (commit db6f171bad) | ConfigWorkflow.__init__() in monai/bundle/workflows.py |
| PyPI | monai | CVE-2026-118684 (GHSA-hhh4-h52m-fqh6) | CVSS v4.0 8.2 High (v3.1 5.9 Medium) | < 1.6.1 | 1.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):
- Unrestricted
eval(act)on the top-levelactivation:key:# ultralytics/nn/tasks.py restricted = _SafeLoad.restricted() if act: Conv.default_act = _SafeLoad.activation(act) if restricted else eval(act) - Arbitrary symbol lookup and instantiation in
d["backbone"] + d["head"]: Each layer entry[f, n, m, args]resolvesmviagetattr(torch.nn, m[3:]),getattr(__import__("torchvision").ops, m[16:]), orglobals()[m], and then callsm(*args)at the end of the loop. WhenrestrictedisFalse, any callable present intasks.py’s global namespace (such astorch_safe_load,check_requirements,check_yaml, orPath) can be invoked with attacker-controlledargs.
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 oftorch.nn.Modulesubclasses (_SafeLoad.loading()). - Refuses to auto-install missing modules on
ModuleNotFoundError. - Parses
activationvia_SafeLoad.activation(act)usingast.parseandast.literal_eval, allowing only[torch.]nn.<Module>(literal_args). - Enforces
isinstance(m, type) and issubclass(m, torch.nn.Module)for every layer inparse_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_datasetinultralytics/data/utils.py): When a dataset.yamlfile specifies atrainorvalpath that does not exist locally and includes adownload:strings,check_det_dataset()executessdirectly 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: S102CVE-2026-118617(ZDI-26-816,load_dataset_cache_fileinultralytics/data/utils.py): When scanning a dataset directory, Ultralytics looks for a precomputed*.cachefile and loads it withnp.load(str(path), allow_pickle=True).item(). Because NumPy’sallow_pickle=Trueinvokes Python’spicklemodule, opening a third-party dataset archive that bundles a crafted.cachefile executes arbitrary code immediately.CVE-2026-118619(ZDI-26-818, CVSS 8.8 High, Google Drive download path traversal inultralytics/utils/downloads.py): Whensafe_download(url, dir=...)is called with ahttps://drive.google.com/...URL andfile=None, it callsget_google_drive_file_info(url), which extracts the filename from the HTTP response’sContent-Dispositionheader usingre.findall('filename="(.+)"', cd)[0]and joins it directly ontodir:
Because# 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)url2file()strips path components viaPath(...).nameon ordinary URLs, butget_google_drive_file_info()returns the rawContent-Dispositionfilenamewithout callingPath(filename).name, a Google Drive file whose name contains../../...(for example../../.bashrcor../../.ssh/authorized_keys) traverses out ofdirand 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
- Upgrade
monaito>= 1.6.1:
When downloading MONAI bundles or datasets viapip install --upgrade "monai>=1.6.1"download_url()ordownload_and_extract(), always supply an explicit SHA-256hash_val. - Harden
ultralyticsdeployments immediately:- Upgrade to
ultralytics >= 8.4.192socheck_requirements()no longer passes requirement strings toshell=True(CVE-2026-118615). - Export
ULTRALYTICS_SAFE_LOAD=1andYOLO_AUTOINSTALL=falsein Dockerfiles, systemd units, and CI jobs so checkpoint loads enforceweights_only=Trueandparse_model()disableseval(act)and non-nn.Moduleglobals (CVE-2026-118613,CVE-2026-118616,CVE-2026-118618). - Before loading any third-party dataset, delete any bundled
*.cachefiles (CVE-2026-118617), inspect the dataset.yamlto confirm it does not define adownload:script (CVE-2026-118614), and never pass untrustedhttps://drive.google.com/...URLs intosafe_download()(CVE-2026-118619).
- Upgrade to
Timeline
- 2026-06-14: Ultralytics merges PR
#24829(5ed793530f) inultralytics 8.4.67, introducing opt-inULTRALYTICS_SAFE_LOAD(_SafeLoad). - 2026-06-18: ZDI reports
ZDI-CAN-29652,30119,30121,30122,30123, and30124(CVE-2026-118613throughCVE-2026-118618) to Ultralytics’s GitHub account. - 2026-08-09: Ultralytics merges PR
#25720(16d2ae072e), removingshell=Truefromcheck_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_hashSHA-256 default) and PR#9114(db6f171bad,_reject_executable_logging_configforGHSA-wvpx-5qmp-46g3), shipping inmonai 1.6.1. - 2026-10-22: After asking for updates on 19 October, ZDI notifies Ultralytics of its intent to publish
ZDI-26-812throughZDI-26-818as zero-day advisories. - 2026-10-28: ZDI and NVD publish
CVE-2026-118613throughCVE-2026-118619(ZDI-26-812throughZDI-26-818), and NVD publishesCVE-2026-118683andCVE-2026-118684for 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
- Zero Day Initiative ZDI-26-812 (CVE-2026-118613: Ultralytics torch_load deserialization RCE)
- Zero Day Initiative ZDI-26-813, ZDI-26-814, ZDI-26-815, ZDI-26-816, and ZDI-26-817 (CVE-2026-118614 through CVE-2026-118618)
- Zero Day Initiative ZDI-26-818 (CVE-2026-118619: Ultralytics Google Drive download path traversal RCE)
- Severity Daily: ZDI published seven unpatched Ultralytics code-execution flaws (CVE-2026-118613 to CVE-2026-118619)
- ultralytics/ultralytics commit 5ed793530f (PR #24829: opt-in ULTRALYTICS_SAFE_LOAD) and commit 16d2ae072e (PR #25720: remove shell=True from check_requirements)
- Project-MONAI/MONAI commit db6f171bad (PR #9114: screen logging.conf class=/args= before fileConfig, CVE-2026-118683 / GHSA-wvpx-5qmp-46g3)
- Project-MONAI/MONAI commit aff7aad661 (PR #9088: harden download/deserialize integrity, CVE-2026-118684 / GHSA-hhh4-h52m-fqh6)
- NVD: CVE-2026-118683 and CVE-2026-118619