T09 · Insecure Skill Coding Practices
- Location
scripts/rebuild_topology_slide.py:35- Finding
Unbounded Decompression of Untrusted PPTX Slide Content
- Content
View full analysis
Vulnerability Details
File Location:
scripts/rebuild_topology_slide.py, lines 35–43
Vulnerability Type: Uncontrolled resource consumption through unbounded ZIP decompression
Risk Level: MediumVulnerable 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.xmlmember 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
- An attacker constructs a valid-looking PPTX ZIP archive.
- The archive includes
ppt/slides/slide1.xmlwith highly repetitive content and an extremely large uncompressed size. - The attacker supplies the PPTX as the source file for the reconstruction workflow.
- The Agent invokes
rebuild_topology_slide.pywith the malicious file. extract_texts()callszf.read(slide_names[0]), expanding the full XML member in process memory. 6 ...[truncated 657 chars]
- Remediation
View remediation
Remediation Suggestions
- Retrieve the selected member's
ZipInfobefore decompression and reject files exceeding a conservative uncompressed-size limit. - Enforce maximum compressed size, uncompressed size, compression ratio, archive member count, and aggregate uncompressed-size limits.
- Replace
ZipFile.read()withZipFile.open()and read in bounded chunks while tracking the total bytes consumed. - Abort immediately when the configured byte budget is exceeded, even if the ZIP metadata claims a smaller size.
- Catch
BadZipFile, decompression errors, oversized-input errors, and decoding failures, then return a controlled error without continuing. - Run document processing with operating-system memory, CPU, and execution-time limits as defense in depth.
- 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")- Retrieve the selected member's
