From 49b09a2a1a33805c879bbdff502a6c6e40c13090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sun, 28 Jun 2026 10:07:45 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20ZIP=20=E2=80=94=20=D0=BB=D0=B8=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=8B=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2/?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0,=20ZIP-=D0=B1=D0=BE?= =?UTF-8?q?=D0=BC=D0=B1=D0=B0,=20=D1=81=D0=B0=D0=BD=D0=B8=D1=82=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=D0=BC=D1=91=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/services/unzip.py | 46 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/deploy/services/unzip.py b/deploy/services/unzip.py index b4b0b67..224f897 100644 --- a/deploy/services/unzip.py +++ b/deploy/services/unzip.py @@ -1,5 +1,23 @@ """Unzip service — extract files from ZIP archive (no DB, no parse).""" -import zipfile, io, base64 +import zipfile, io, base64, os + +MAX_FILES = 500 +MAX_UNCOMPRESSED = 500 * 1024 * 1024 # 500 MB total +MAX_RATIO = 100 # max compression ratio (uncompressed/compressed) + + +def _sanitize_zip_name(name: str) -> str | None: + """Decode cp437→utf8, strip path traversal, return safe name or None.""" + # Decode cp437 filenames (common in ZIPs from Windows) + try: + name = name.encode("cp437").decode("utf-8", errors="replace") + except (UnicodeDecodeError, UnicodeEncodeError): + pass + # Strip directory prefixes and path traversal + name = os.path.basename(name) + if not name or name.endswith("/") or ".." in name or "/" in name or "\\" in name: + return None + return name def handle_unzip(rfile, content_length): @@ -19,11 +37,31 @@ def handle_unzip(rfile, content_length): zip_data = raw[zip_start:] files = [] + total_uncompressed = 0 with zipfile.ZipFile(io.BytesIO(zip_data)) as zf: - for name in zf.namelist(): - if name.endswith("/"): + namelist = zf.namelist() + if len(namelist) > MAX_FILES: + return {"ok": False, "error": f"too many files in ZIP (max {MAX_FILES})"} + + for info in zf.infolist(): + if info.is_dir(): continue - data = zf.read(name) + + # ZIP bomb: compression ratio check + if info.compress_size > 0: + ratio = info.file_size / info.compress_size + if ratio > MAX_RATIO: + return {"ok": False, "error": f"suspicious compression ratio ({ratio:.0f}:1) — possible ZIP bomb"} + + name = _sanitize_zip_name(info.filename) + if not name: + continue # skip unsafe names + + data = zf.read(info) + total_uncompressed += len(data) + if total_uncompressed > MAX_UNCOMPRESSED: + return {"ok": False, "error": f"uncompressed size exceeds {MAX_UNCOMPRESSED // 1024 // 1024}MB"} + ext = name.rsplit(".", 1)[-1].lower() if "." in name else "" files.append({ "filename": name,