Handle malformed DOCX/ODT beyond BadZipFile in container inspectors (#92)

* Handle malformed DOCX/ODT beyond BadZipFile in container inspectors

A corrupt, truncated, encrypted, or unsupported-compression container
raises more than BadZipFile (NotImplementedError, RuntimeError, EOFError,
OSError, ValueError, zlib.error), so inspect_docx / inspect_odt and
detect_container_format now catch the whole family and degrade to a clear
finding.

The zip-bomb rejection in _check_zip_budget now raises a dedicated
ZipBudgetExceeded so it keeps propagating out of the inspectors, matching
clean_docx / clean_odt, instead of being reported as an unparseable zip.

* fix: satisfy ruff lint and format checks

---------

Co-authored-by: eeshsaxena <chinmaymd72@gmail.com>
Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
This commit is contained in:
eeshsaxena
2026-08-17 16:16:38 -07:00
committed by GitHub
co-authored by eeshsaxena Guillaume Meyer
parent cc7444eed2
commit 00596a7270
3 changed files with 61 additions and 6 deletions
+31 -4
View File
@@ -11,6 +11,7 @@ import re
import subprocess
import urllib.parse
import zipfile
import zlib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@@ -46,6 +47,32 @@ from image_meta import (
detect_format as detect_image_format,
)
class ZipBudgetExceeded(Exception):
"""A zip's declared decompressed size exceeds the processing cap.
Kept separate from the parse errors below so a refused zip bomb keeps
propagating (as it already does out of the clean_* helpers) instead of
being reported as an unparseable container.
"""
# A corrupt, truncated, encrypted, or unsupported-compression zip surfaces as
# more than just BadZipFile: reading a member can raise NotImplementedError
# (unknown compression/version), RuntimeError (encrypted), zlib.error (bad
# deflate stream), EOFError (truncated), OSError (invalid stream), or
# ValueError (e.g. a negative seek).
_ZIP_PARSE_ERRORS = (
zipfile.BadZipFile,
zipfile.LargeZipFile,
NotImplementedError,
RuntimeError,
EOFError,
OSError,
ValueError,
zlib.error,
)
# Frontmatter / meta keys that often carry AI provenance
AI_FRONTMATTER_KEYS = frozenset(
{
@@ -162,7 +189,7 @@ def detect_container_format(path: Path, data: bytes | None = None) -> str:
return "odt"
if "META-INF/container.xml" in names and any(n.endswith(".opf") for n in names):
return "epub"
except zipfile.BadZipFile:
except _ZIP_PARSE_ERRORS:
pass
return "unknown"
@@ -666,7 +693,7 @@ def _check_zip_budget(info: zipfile.ZipInfo, budget: list[int]) -> None:
"""Reject zip bombs before decompression (ZipInfo.file_size is stored)."""
budget[0] += info.file_size
if budget[0] > MAX_ZIP_DECOMPRESSED_BYTES:
raise ValueError(
raise ZipBudgetExceeded(
"zip decompressed size exceeds cap "
f"({MAX_ZIP_DECOMPRESSED_BYTES} bytes); refusing to process"
)
@@ -739,7 +766,7 @@ def _inspect_ooxml_zip(data: bytes, fmt: str) -> tuple[bool, bool, list[str], di
custom = [n for n in parts if n.startswith("customXml/")]
if custom:
findings.append(f"customXml parts: {len(custom)}")
except zipfile.BadZipFile:
except _ZIP_PARSE_ERRORS:
return False, False, [f"not a valid {fmt.upper()} zip"], {}
return has_c2pa, has_ai or has_c2pa, findings, {"parts": len(parts)}
@@ -1069,7 +1096,7 @@ def inspect_odt(data: bytes) -> tuple[bool, bool, list[str], dict]:
if re.search(r"generator|claude|openai|anthropic|gemini", meta, re.I):
has_ai = True
findings.append("meta.xml generator-like fields")
except zipfile.BadZipFile:
except _ZIP_PARSE_ERRORS:
return False, False, ["not a valid ODT zip"], {}
return has_c2pa, has_ai or has_c2pa, findings, {}