All vulnerabilities
GHSA-vm9c-7j6g-c7mm

monai: RCE via SENet pretrained weights loaded over plaintext HTTP and unpickled without weights_only=True

monai
CVSS
7.5High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in1.6.1Fixed versionGitHub
Published27 Sep 2026Updated 27 Sep 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

MONAI's SENet family fetches pretrained weights over plaintext HTTP and then deserializes them with torch.hub.load_state_dict_from_url, which defaults weights_only=False (full pickle) on all supported torch versions (verified on torch 2.13.0). SEResNet50(pretrained=True) (and the other SENet variants) therefore performs an unauthenticated, integrity-unchecked download from http://data.lip6.fr/... and unpickles it, so an on-path attacker who substitutes a crafted .pth achieves remote code execution at model-construction time. The sibling torch.load branch in the same function uses weights_only=True, but that branch is unreachable for built-in models — the reachable load_state_dict_from_url branch was never hardened.

This same "pretrained loaders omit weights_only=True" root cause is also present (over HTTPS transport) at monai/networks/nets/efficientnet.py:796 (model_zoo.load_url), monai/networks/nets/densenet.py:280 (load_state_dict_from_url), and monai/networks/blocks/text_embedding.py:70 (model_zoo.load_url); those call-sites share the identical one-line fix and are listed here as additional affected locations. The SENet path is the most severe because its transport is plaintext HTTP (trivial MITM), whereas the others require compromising a TLS-secured distribution host/account.

Root Cause

  • senet.py SE_NET_MODELS (lines 41-46): all six pretrained URLs are http://data.lip6.fr/... (no TLS).
  • _load_state_dict (line ~285): model_url = look_up_option(arch, SE_NET_MODELS, None). Every allowlisted value is a str, so isinstance(model_url, dict) is always False for built-in models → the reachable branch is else at line 307: load_state_dict_from_url(model_url, progress=progress). The safe sibling at line 305 (torch.load(..., weights_only=True)) is dead for built-in models.
  • torch.hub.load_state_dict_from_url and torch.utils.model_zoo.load_url both forward their own default weights_only=False to torch.load → full unpickle. Confirmed via inspect.signature on torch 2.13.0.
  • No hash/integrity check is applied on these branches (download_url/hash_val not used).

Impact

Remote code execution at model-construction time on the victim host for the SENet plaintext-HTTP path (on-path MITM). For the HTTPS call-sites (efficientnet/densenet/text_embedding), RCE requires distribution-host/account or registry compromise (supply-chain), or a shared-writable torch hub cache.

Proof of Concept

  1. Victim calls SEResNet50(spatial_dims=2, pretrained=True).
  2. MONAI fetches http://data.lip6.fr/.../se_resnet50-ce0d4300.pth over plaintext HTTP.
  3. On-path attacker (rogue Wi-Fi/proxy, ARP/DNS spoof, BGP hijack, compromised mirror) returns a crafted .pth whose pickle payload runs on load.
  4. load_state_dict_from_url → torch.load(bytes, weights_only=False) → __reduce__ RCE.

Attack Chain

  1. Entry: victim calls SEResNet50(..., pretrained=True) (documented public API). Guard: look_up_option(arch, SE_NET_MODELS) allowlist. Bypass proof: allowlist only maps name→URL; every allowlisted URL is plaintext HTTP.
  2. Transport: MONAI fetches http://data.lip6.fr/.../se_resnet50-ce0d4300.pth. Guard: none — no TLS, no hash. Bypass proof: scheme is http:// at senet.py:42; on-path attacker returns crafted .pth.
  3. Sink: load_state_dict_from_url → torch.load(bytes, weights_only=False) → full unpickling → __reduce__ RCE. Bypass proof: inspect.signature(torch.hub.load_state_dict_from_url) shows weights_only: bool = False on torch 2.13.0; sibling senet.py:305 uses weights_only=True, senet.py:307 does not.
  4. Impact: RCE at model-construction time.

Bypass Evidence

  • python3 -c "import inspect,torch.hub;print(inspect.signature(torch.hub.load_state_dict_from_url))" → ... weights_only: bool = False ... on torch 2.13.0 (and defaults False on all supported torch versions).
  • SE_NET_MODELS URLs are http:// (plaintext) at senet.py:41-46.
  • Asymmetry: senet.py:305 (dict branch, dead for built-in models) uses weights_only=True; senet.py:307 (reachable branch) does not.

Affected Versions

<= 1.6.0 (all sinks confirmed present at the 1.6.0 release tag via git show 1.6.0:<file>; no hardening commit on the reachable branch). Not covered by GHSA-6vm5-6jv9-rjpj, whose fix scope is direct torch.load in the CheckpointLoader path (affected <= 1.5.0); load_state_dict_from_url / model_zoo.load_url were never swept.

Suggested Fix

Pass weights_only=True to every pretrained-weights loader call (load_state_dict_from_url at senet.py:307 and densenet.py:280; model_zoo.load_url at efficientnet.py:796 and text_embedding.py:70), switch the SENet URLs to HTTPS, and route downloads through download_url(..., hash_val=...) to enforce integrity.

--- Reported by zx (Jace) — GitHub: @manus-use

Text from GitHub Advisory Database

Affected versions 1

  • monai, pip<= 1.6.0GitHub