自动化交付

处理 DOCX 和 EPUB 前,先给 ZIP 解压设预算

DOCX 本质上是一组 ZIP 条目,EPUB 也采用 ZIP 容器。收到一个体积很小的压缩文件,不代表解开以后仍然小。入口处应限制压缩体积、条目数量、单项大小与总展开量,而不是等内存用完才判断文件不正常。

阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。 DOCX 本质上是一组 ZIP 条目,EPUB 也采用 ZIP 容器。收到一个体积很小的压缩文件,不代表解开以后仍然小。入口处应限制压缩体积、条目数量、单项大小与总展开量,而不是等内存用完才判断文件不正常。 先检查目录表,但别把它当最终保证 ZipInfo 提供声明大小与压缩大小,适合做预筛。实际解压时仍要累计已经读出的字节,并设置时间与内存预算。目录表可以帮助提前拒绝,却不能代替运行时限制。 一个不解压的预筛示例 from io import BytesIO from pathlib import PurePosixPath from zipfile import ZipFile, ZIP_DEFLATED def inspect(raw, max_entries=10, max_total=4096): if len(raw) > 4096: raise ValueError("compressed input budget") with ZipFile(BytesIO(raw)) as z: items = z.infolist() if len(items) > max_entries: raise ValueError("entry count budget") total, seen = 0, set() for item in items: name = item.filename path = PurePosixPath(name) if (path.is_absolute() or ".." in path.parts or "\\" in name or ":" in name or name in seen): raise ValueError("unsafe or duplicate name") seen.add(name) total += item.file_size if total > max_total: raise ValueError("expanded budget") return len(items), total def sample(name, data): out = BytesIO() with ZipFile(out, "w", ZIP_DEFLATED) as z: z.writestr(name, data) return out.getvalue() assert inspect(sample("word/document.xml", b"<doc/>")) == (1, 6) for raw in [sample("../escape", b"x"), sample("large", b"x" * 5000)]: try: inspect(raw) except ValueError: pass else: raise AssertionError("unsafe archive accepted") 这些预算是为了演示拒绝路径而选的很小数值,不能直接复制为生产限制。示例不调用 extractall ,也不实施 ZIP 条目写入,因此没有声称完成安全解压器。加密条目、符号链接、嵌套归档和解析器自身资源消耗都需要单独策略。 拒绝时给用户可行动的信息 触发条件 对用户说明 内部记录 总展开量超预算 文件展开后过大 限制名与观测值 重复条目名 容器结构不明确 条目索引,不记全文 越界路径 文件结构不受支持 安全事件关联 ID 容器损坏 请重新导出文件 解析异常类型 不要为了兼容关闭所有限制 合理的大型文档也可能超过预算,应提供独立的大文件队列或人工处理入口。提高一个租户的业务配额,不等于允许所有任务无限解压。限制需要与执行隔离配合,而不是只写在上传按钮旁边。 翻译失败要分清在哪一层 准备向 OmniDoc(产品入口) 提交文档时,先把容器损坏与翻译失败分开。一个连 XML 都读不出的 DOCX,问题可能发生在输入准备阶段。清楚的预检报告能让用户知道应该重新导出、压缩图片,还是联系翻译服务排查。 参考: Python zipfile 。 - 1 -
更新于 2026-10-08