T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/rebuild_topology_slide.py:35
- Finding
- Unbounded Decompression of Untrusted PPTX Slide Content## Vulnerability Details **File Location**: `scripts/rebuild_topology_slide.py`, lines 35–43 **Vulnerability Type**: Uncontrolled resource consumption through unbounded ZIP decompression **Risk Level**: Medium ### Vulnerable Code ```python def extract_texts(pptx_path: Path) -> list[str]: texts: list[str] = [] with ZipFile(pptx_path) as zf: slide_names = sorted( n for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml") ) if not slide_names: return texts data = zf.read(slide_names[0]).decode("utf-8", "ignore") ``` ### Technical Analysis PPTX files are ZIP archives and may originate from untrusted users. The function identifies the first slide XML member and passes its name directly to `ZipFile.read()`. This operation decompresses the entire member into memory before decoding or processing it. The implementation does not enforce a maximum archive size, member count, uncompressed member size, compression ratio, or total decompression budget. It also does not use bounded streaming. Consequently, a small PPTX archive can contain a highly compressed but extremely large `ppt/slides/slide1.xml` member that consumes excessive memory when expanded. This is a resource-exhaustion weakness rather than arbitrary code execution. The regular-expression operation performed afterward may further increase CPU and memory consumption, but complete decompression already occurs first. ### Attack Path 1. An attacker constructs a valid-looking PPTX ZIP archive. 2. The archive includes `ppt/slides/slide1.xml` with highly repetitive content and an extremely large uncompressed size. 3. The attacker supplies the PPTX as the source file for the reconstruction workflow. 4. The Agent invokes `rebuild_topology_slide.py` with the malicious file. 5. `extract_texts()` calls `zf.read(slide_names[0])`, expanding the full XML member in process memory. 6 ...[truncated 657 chars]
- Remediation
- ## Remediation Suggestions 1. Retrieve the selected member's `ZipInfo` before decompression and reject files exceeding a conservative uncompressed-size limit. 2. Enforce maximum compressed size, uncompressed size, compression ratio, archive member count, and aggregate uncompressed-size limits. 3. Replace `ZipFile.read()` with `ZipFile.open()` and read in bounded chunks while tracking the total bytes consumed. 4. Abort immediately when the configured byte budget is exceeded, even if the ZIP metadata claims a smaller size. 5. Catch `BadZipFile`, decompression errors, oversized-input errors, and decoding failures, then return a controlled error without continuing. 6. Run document processing with operating-system memory, CPU, and execution-time limits as defense in depth. 7. Consider parsing XML incrementally after bounded decompression rather than loading the complete slide XML into memory. Example hardening pattern: ```python from zipfile import BadZipFile, ZipFile MAX_SLIDE_XML_BYTES = 10 * 1024 * 1024 MAX_COMPRESSION_RATIO = 100 with ZipFile(pptx_path) as zf: slide_names = sorted( n for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml") ) if not slide_names: return [] info = zf.getinfo(slide_names[0]) compressed_size = max(info.compress_size, 1) if ( info.file_size > MAX_SLIDE_XML_BYTES or info.file_size / compressed_size > MAX_COMPRESSION_RATIO ): raise ValueError("PPTX slide XML exceeds safety limits") with zf.open(info) as source: data = source.read(MAX_SLIDE_XML_BYTES + 1) if len(data) > MAX_SLIDE_XML_BYTES: raise ValueError("PPTX slide XML exceeds safety limits") xml_text = data.decode("utf-8", "ignore") ```
