"""Safe ZIP extraction with Zip Slip protection and filename normalization.""" import os import zipfile from pathlib import Path def normalize_zip_filenames(zipf: zipfile.ZipFile) -> None: """Repara nombres de archivos UTF-8 en ZIPs sin el flag UTF-8 seteado. Args: zipf: Objeto ZipFile abierto en modo lectura. Returns: None. Modifica los infolist del ZipFile in-place. """ def _is_cjk(s: str) -> bool: return any( "\u3400" <= c <= "\u4dbf" or "\u4e00" <= c <= "\u9fff" or "\u3000" <= c <= "\u303f" or "\uff00" <= c <= "\uffef" for c in s ) def _is_mojibake(s: str) -> bool: return any( "\u0370" <= c <= "\u03ff" # Greek or "\u2200" <= c <= "\u22ff" # Math or "\u2500" <= c <= "\u257f" # Box Drawing for c in s ) repaired = False for info in zipf.infolist(): # Flag 0x800 indica que el filename ya esta en UTF-8 if info.flag_bits & 0x800: continue try: repaired_name = info.filename.encode("cp437").decode("utf-8") if _is_cjk(repaired_name) and _is_mojibake(info.filename): info.filename = repaired_name repaired = True except (UnicodeEncodeError, UnicodeDecodeError): pass if repaired: zipf.metadata_encoding = "utf-8" def safe_extract_zip(zip_path: str, dest_dir: str) -> None: """Extrae un archivo ZIP con proteccion contra Zip Slip (path traversal). Valida que cada archivo extraido quede dentro del directorio destino antes de extraerlo. Normaliza los nombres de archivo UTF-8 antes de extraer. Args: zip_path: Ruta al archivo ZIP a extraer. dest_dir: Directorio de destino para la extraccion. Raises: ValueError: Si se detecta un intento de Zip Slip (path traversal). zipfile.BadZipFile: Si el archivo no es un ZIP valido. FileNotFoundError: Si zip_path no existe. """ dest = Path(dest_dir).resolve() with zipfile.ZipFile(zip_path, "r") as zipf: normalize_zip_filenames(zipf) for member in zipf.infolist(): member_path = (dest / member.filename).resolve() # Verificar que el path resultante este dentro de dest_dir if not str(member_path).startswith(str(dest) + os.sep): raise ValueError( f"Zip Slip attempt detected: {member.filename!r} would extract to {member_path}" ) zipf.extract(member, dest)