Skip to content
36 changes: 36 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Security Policy

## Reporting a Vulnerability
MONAI takes security seriously and appreciate your efforts to responsibly disclose vulnerabilities. If you discover a security issue, please report it as soon as possible.

To report a security issue:
* please use the GitHub Security Advisories tab to "[Open a draft security advisory](https://github.com/Project-MONAI/MONAI/security/advisories/new)".
* Include a detailed description of the issue, steps to reproduce, potential impact, and any possible mitigations.
* If applicable, please also attach proof-of-concept code or screenshots.
* We aim to acknowledge your report within 72 hours and provide a status update as we investigate.
* Please do not create public issues for security-related reports.

## Disclosure Policy
* We follow a coordinated disclosure approach.
* We will not publicly disclose vulnerabilities until a fix has been developed and released.
* Credit will be given to researchers who responsibly disclose vulnerabilities, if requested.

## Acknowledgements
We greatly appreciate contributions from the security community and strive to recognize all researchers who help keep MONAI safe.

# Reporting a Vulnerability
At MONAI, we take security seriously and appreciate your efforts to responsibly disclose vulnerabilities. If you discover a security issue, please report it as soon as possible.

Please do not create public issues for security-related reports.

* To report a security issue, please use the GitHub Security Advisories tab to "Open a draft security advisory".
* Include a detailed description of the issue, steps to reproduce, potential impact, and any possible mitigations.
* If applicable, please also attach proof-of-concept code or screenshots.
* We will acknowledge your report within 72 hours and provide a status update as we investigate.

# Disclosure Policy
* We follow a coordinated disclosure approach.
* We will not publicly disclose vulnerabilities until a fix has been developed and released.
* Credit will be given to researchers who responsibly disclose vulnerabilities, if requested.
# Acknowledgements
We greatly appreciate contributions from the security community and strive to recognize all researchers who help keep MONAI safe.
56 changes: 47 additions & 9 deletions monai/apps/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

from __future__ import annotations

import os
import shutil
import hashlib
import json
import logging
import os
import re
import shutil
import sys
import tarfile
import tempfile
Expand Down Expand Up @@ -80,7 +80,6 @@ def get_logger(
logger = get_logger("monai.apps")
__all__.append("logger")


def _basename(p: PathLike) -> str:
"""get the last part of the path (removing the trailing slash if it exists)"""
sep = os.path.sep + (os.path.altsep or "") + "/ "
Expand Down Expand Up @@ -121,6 +120,31 @@ def update_to(self, b: int = 1, bsize: int = 1, tsize: int | None = None) -> Non
logger.error(f"Download failed from {url} to {filepath}.")
raise e

def safe_extract_member(member, extract_to):
"""Securely verify compressed package member paths to prevent path traversal attacks"""
# Get member path (handle different compression formats)
if hasattr(member, 'filename'):
member_path = member.filename # zipfile
elif hasattr(member, 'name'):
member_path = member.name # tarfile
else:
member_path = str(member)

member_path = os.path.normpath(member_path)

if os.path.isabs(member_path) or '..' in member_path.split(os.sep):
raise ValueError(f"Unsafe path detected in archive: {member_path}")

full_path = os.path.join(extract_to, member_path)
full_path = os.path.normpath(full_path)

extract_to_abs = os.path.abspath(extract_to)
full_path_abs = os.path.abspath(full_path)

if not (full_path_abs == extract_to_abs or full_path_abs.startswith(extract_to_abs + os.sep)):
raise ValueError(f"Path traversal attack detected: {member_path}")

return full_path

def check_hash(filepath: PathLike, val: str | None = None, hash_type: str = "md5") -> bool:
"""
Expand Down Expand Up @@ -287,14 +311,28 @@ def extractall(
logger.info(f"Writing into directory: {output_dir}.")
_file_type = file_type.lower().strip()
if filepath.name.endswith("zip") or _file_type == "zip":
zip_file = zipfile.ZipFile(filepath)
zip_file.extractall(output_dir)
zip_file.close()
with zipfile.ZipFile(filepath, 'r') as zip_file:
for member in zip_file.infolist():
if member.is_dir():
continue
safe_path = safe_extract_member(member, output_dir)
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
with zip_file.open(member) as source:
with open(safe_path, 'wb') as target:
shutil.copyfileobj(source, target)
return
if filepath.name.endswith("tar") or filepath.name.endswith("tar.gz") or "tar" in _file_type:
tar_file = tarfile.open(filepath)
tar_file.extractall(output_dir)
tar_file.close()
with tarfile.open(filepath, 'r') as tar_file:
for member in tar_file.getmembers():
if not member.isfile():
continue

safe_path = safe_extract_member(member, output_dir)
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
with tar_file.extractfile(member) as source:
if source:
with open(safe_path, 'wb') as target:
shutil.copyfileobj(source, target)
return
raise NotImplementedError(
f'Unsupported file type, available options are: ["zip", "tar.gz", "tar"]. name={filepath} type={file_type}.'
Expand Down
Loading