monai: RCE via SENet pretrained weights loaded over plaintext HTTP and unpickled without weights_only=True
What fixes it
- 1.6.1monai, pipGitHub
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.pySE_NET_MODELS(lines 41-46): all six pretrained URLs arehttp://data.lip6.fr/...(no TLS)._load_state_dict(line ~285):model_url = look_up_option(arch, SE_NET_MODELS, None). Every allowlisted value is astr, soisinstance(model_url, dict)is always False for built-in models → the reachable branch iselseat 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_urlandtorch.utils.model_zoo.load_urlboth forward their own defaultweights_only=Falsetotorch.load→ full unpickle. Confirmed viainspect.signatureon torch 2.13.0.- No hash/integrity check is applied on these branches (
download_url/hash_valnot 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
- Victim calls
SEResNet50(spatial_dims=2, pretrained=True). - MONAI fetches
http://data.lip6.fr/.../se_resnet50-ce0d4300.pthover plaintext HTTP. - On-path attacker (rogue Wi-Fi/proxy, ARP/DNS spoof, BGP hijack, compromised mirror) returns a crafted
.pthwhose pickle payload runs on load. load_state_dict_from_url→torch.load(bytes, weights_only=False)→__reduce__RCE.
Attack Chain
- 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. - Transport: MONAI fetches
http://data.lip6.fr/.../se_resnet50-ce0d4300.pth. Guard: none — no TLS, no hash. Bypass proof: scheme ishttp://at senet.py:42; on-path attacker returns crafted.pth. - 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)showsweights_only: bool = Falseon torch 2.13.0; sibling senet.py:305 usesweights_only=True, senet.py:307 does not. - 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_MODELSURLs arehttp://(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