T09 · Insecure Skill Coding Practices
- Location
scripts/convert_to_house_style.py:129- Finding
Unbounded DOCX Archive Decompression Enables Denial of Service
- Content
View full analysis
Vulnerability Details
File Location:
scripts/convert_to_house_style.py, lines 129-131
Vulnerability Type: Uncontrolled resource consumption through ZIP bomb decompression
Risk Level: MediumVulnerable Code
python def read_zip_entries(docx_path: Path) -> dict[str, bytes]: with zipfile.ZipFile(docx_path, "r") as archive: return {name: archive.read(name) for name in archive.namelist()}Technical Analysis
A DOCX file is a ZIP archive whose contents are controlled by the document provider. The converter enumerates every archive member and fully decompresses each member into memory. All resulting byte strings are retained simultaneously in a dictionary.
No validation or limits are applied to:
- The number of archive members
- The uncompressed size of an individual member
- The aggregate uncompressed size
- The ratio between compressed and uncompressed sizes
- Processing time or memory consumption
The
.docxfilename-suffix check does not establish that the archive is safe. An attacker can construct a small, highly compressed DOCX whose members expand to a very large size. The same resource-exhaustion condition can be caused by an archive containing a very large number of members.Attack Path
- An attacker creates a ZIP bomb or another oversized archive and gives it a
.docxextension. - The attacker submits the file for conversion through the Skill's normal workflow.
- The workflow invokes
convert_to_house_style.py. convert()callsread_zip_entries()before applying any archive resource limits.archive.read(name)decompresses every member, while the dictionary comprehension retains every decompressed payload in memory.- Memory and CPU consumption increase until the process slows substantially, raises an exception, or is terminated by the operating system.
- If conversion runs in a shared service, concurrent users or other workloads on the same worker may also become unavailable.
Imp
...[truncated 766 chars]
- Remediation
View remediation
Remediation Suggestions
Validate the archive before decompressing any member:
- Set a conservative maximum number of ZIP members.
- Reject members whose declared uncompressed size exceeds a per-entry limit.
- Reject archives whose aggregate declared uncompressed size exceeds a total limit.
- Reject suspicious compression ratios, while accounting for empty or zero-byte compressed members.
- Permit only DOCX-relevant member names and reject malformed or duplicate critical parts.
- Read only the OOXML parts required for conversion rather than eagerly loading the entire archive.
- Stream unchanged members directly from the source archive to the output archive where practical.
- Apply process-level memory and execution-time limits as defense in depth.
- Return a clear validation error when limits are exceeded.
For example, perform metadata validation before reading payloads:
python MAX_ENTRIES = 2_000 MAX_ENTRY_SIZE = 50 * 1024 * 1024 MAX_TOTAL_SIZE = 200 * 1024 * 1024 MAX_COMPRESSION_RATIO = 100 def validate_archive(archive: zipfile.ZipFile) -> None: members = archive.infolist() if len(members) > MAX_ENTRIES: raise ValueError("DOCX contains too many archive entries") total_size = 0 for member in members: if member.file_size > MAX_ENTRY_SIZE: raise ValueError(f"DOCX entry is too large: {member.filename}") total_size += member.file_size if total_size > MAX_TOTAL_SIZE: raise ValueError("DOCX exceeds the total uncompressed-size limit") if member.file_size and member.compress_size == 0: raise ValueError(f"Invalid compressed entry: {member.filename}") if member.compress_size: ratio = member.file_size / member.compress_size if ratio > MAX_COMPRESSION_RATIO: raise ValueError( f"Suspicious compression ratio for entry: {member.filename}" ) def read_zip_entries(docx_path: Path) -> ...[truncated 378 chars]
