T09 · Insecure Skill Coding Practices
Warning
- Location
- tools/verify_source_sync.py:47
- Finding
- Unbounded ZIP Decompression Enables Memory Exhaustion## Vulnerability Details **File Location**: `tools/verify_source_sync.py`, lines 47-54 **Vulnerability Type**: Unbounded archive decompression and resource exhaustion **Risk Level**: Medium ### Vulnerable Code ```python def package_files(pkg): out = {} with zipfile.ZipFile(pkg) as zf: for n in zf.namelist(): if not n.startswith(ZIP_PREFIX): continue rel = n[len(ZIP_PREFIX):] if rel.endswith('/'): continue out[rel] = zf.read(n) return out ``` ### Technical Analysis The release-verification utility accepts a caller-selected ZIP archive and reads every matching entry completely into memory using `ZipFile.read()`. All decompressed entries are retained simultaneously in the `out` dictionary. The implementation does not enforce limits on: - The number of archive entries - Individual uncompressed file size - Total uncompressed archive size - Compression ratio - Process memory consumption - Duplicate or ambiguously normalized entry names A small, highly compressed ZIP bomb can therefore expand to a very large amount of data during verification. Because all expanded content remains resident in memory, a malicious archive can exhaust available memory and terminate the Python process or destabilize the host. The `market-scout/` prefix check does not mitigate this issue because an attacker can place oversized entries under that prefix. This is a denial-of-service flaw rather than a code-execution vulnerability. The reviewed implementation reads archive members without extracting them to the filesystem, so no ZIP path-traversal write was established from this code. ### Attack Path 1. An attacker creates a ZIP archive containing one or more highly compressed files beneath the expected `market-scout/` prefix. 2. The attacker supplies the archive to a maintainer or CI workflow as a purported Market Scout rele ...[truncated 1121 chars]
- Remediation
- ## Remediation Suggestions Harden archive processing before reading any member: 1. Inspect each `ZipInfo` record before decompression. 2. Enforce a conservative maximum entry count. 3. Reject entries whose declared uncompressed size exceeds a per-file limit. 4. Track and limit cumulative uncompressed size across the archive. 5. Reject suspicious compression ratios, including entries with very small compressed sizes and extremely large uncompressed sizes. 6. Reject encrypted entries and unsupported compression methods. 7. Detect duplicate normalized paths to prevent one entry from silently replacing another in the dictionary. 8. Stream each member in bounded chunks and compute a digest instead of retaining all decompressed files in memory. 9. Run package verification in a resource-limited CI container with memory, CPU, and execution-time limits. 10. Treat ZIP metadata as untrusted and abort safely when any size or structural limit is exceeded. A hardened implementation should validate metadata and stream hashes, for example: ```python import hashlib import zipfile MAX_ENTRIES = 500 MAX_FILE_SIZE = 20 * 1024 * 1024 MAX_TOTAL_SIZE = 200 * 1024 * 1024 MAX_RATIO = 100 CHUNK_SIZE = 64 * 1024 def package_files(pkg): out = {} total_size = 0 with zipfile.ZipFile(pkg) as zf: infos = zf.infolist() if len(infos) > MAX_ENTRIES: raise ValueError("Archive contains too many entries") for info in infos: name = info.filename if not name.startswith(ZIP_PREFIX): continue rel = name[len(ZIP_PREFIX):] if not rel or rel.endswith("/"): continue if rel in out: raise ValueError("Duplicate archive path: " + rel) if info.flag_bits & 0x1: raise ValueError("Encrypted entries are not allowed") if info.file_size > M ...[truncated 1154 chars]
