T09 · Insecure Skill Coding Practices
- Location
tools/verify_source_sync.py:47- Finding
Unbounded ZIP Decompression Enables Memory Exhaustion
- Content
View full analysis
Vulnerability Details
File Location:
tools/verify_source_sync.py, lines 47-54
Vulnerability Type: Unbounded archive decompression and resource exhaustion
Risk Level: MediumVulnerable 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 outTechnical 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 theoutdictionary.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
- An attacker creates a ZIP archive containing one or more highly compressed files beneath the expected
market-scout/prefix. - The attacker supplies the archive to a maintainer or CI workflow as a purported Market Scout rele ...[truncated 1121 chars]
- Remediation
View remediation
Remediation Suggestions
Harden archive processing before reading any member:
- Inspect each
ZipInforecord before decompression. - Enforce a conservative maximum entry count.
- Reject entries whose declared uncompressed size exceeds a per-file limit.
- Track and limit cumulative uncompressed size across the archive.
- Reject suspicious compression ratios, including entries with very small compressed sizes and extremely large uncompressed sizes.
- Reject encrypted entries and unsupported compression methods.
- Detect duplicate normalized paths to prevent one entry from silently replacing another in the dictionary.
- Stream each member in bounded chunks and compute a digest instead of retaining all decompressed files in memory.
- Run package verification in a resource-limited CI container with memory, CPU, and execution-time limits.
- 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]- Inspect each
