{"record":{"id":"93491881c67d6c3c","repo":"anthropics/skills","slug":"unsafe-archive-entry-m-filename-r","errorCode":null,"errorMessage":"unsafe archive entry: {m.filename!r}","messagePattern":"unsafe archive entry: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/docx/scripts/office/helpers/__init__.py","lineNumber":81,"sourceCode":"def part_text(data: bytes) -> str:\n    return data.decode(\"utf-8\", \"surrogateescape\")\n\n\nXML_SPACE = \" \\t\\r\\n\"\n\n\ndef rendered_text(text: str, preserve: bool) -> str:\n    return text if preserve else text.strip(XML_SPACE)\n\n\ndef safe_extract(zf: zipfile.ZipFile, dest: Path) -> None:\n    dest = dest.resolve()\n    for m in zf.infolist():\n        if stat.S_ISLNK(m.external_attr >> 16):\n            raise ValueError(f\"symlink archive entry not allowed: {m.filename!r}\")\n        target = (dest / m.filename).resolve()\n        if not target.is_relative_to(dest):\n            raise ValueError(f\"unsafe archive entry: {m.filename!r}\")\n        zf.extract(m, dest)\n\n\ndef rezip(src_dir: Path, out_path: Path) -> None:\n    files = sorted(p for p in src_dir.rglob(\"*\") if p.is_file())\n    ct = src_dir / \"[Content_Types].xml\"\n    fd, tmp_name = tempfile.mkstemp(\n        prefix=out_path.name + \".\", suffix=\".tmp\", dir=out_path.parent\n    )\n    tmp_out = Path(tmp_name)\n    try:\n        with os.fdopen(fd, \"wb\") as fh:\n            with zipfile.ZipFile(fh, \"w\", zipfile.ZIP_DEFLATED) as zf:\n                if ct.exists():\n                    zf.write(ct, ct.relative_to(src_dir), compress_type=zipfile.ZIP_STORED)\n                for f in files:\n                    if f == ct:\n                        continue","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/anthropics/skills/blob/f6656c1256d5a8adfa37db9110046ef20bac644c/skills/docx/scripts/office/helpers/__init__.py#L63-L99","documentation":"safe_extract() raises this when an archive entry name resolves (after dest resolution) to a path outside the destination directory — absolute names like '/etc/cron.d/x' or traversal names like '../../home/u/.bashrc'. It is the classic zip-slip defense; without it, extraction could overwrite arbitrary files. Present in both docx and pptx office/helpers.","triggerScenarios":"Unpacking a crafted or corrupted OOXML/zip whose member filename is absolute ('/tmp/evil') or contains '..' segments escaping dest; also seen when a non-OOXML zip is mistakenly fed to the unpack flow.","commonSituations":"Malicious uploads targeting naive extract(); archives produced by buggy writers that mangle names; processing random zips found in the wild with tooling that assumes Office files.","solutions":["Reject and quarantine the input — the extraction guard fired on a genuinely dangerous or malformed archive.","Inspect the offending entry name from the message and, if legitimate corruption, rebuild the archive with normalized relative names.","Pre-scan archives before processing (check for absolute names or '..' segments) so malicious files never reach the tool.","Ensure your pipeline only feeds .docx/.pptx from trusted sources through these scripts."],"exampleFix":"# before\nwith zipfile.ZipFile(path) as zf:\n    zf.extractall(dest)  # zip-slip vulnerable\n\n# after\nfrom office.helpers import safe_extract\nwith zipfile.ZipFile(path) as zf:\n    safe_extract(zf, Path(dest))  # raises on escaping entries","handlingStrategy":"validation","validationCode":"import zipfile\n\ndef has_escaping_entries(path: str) -> bool:\n    with zipfile.ZipFile(path) as zf:\n        return any(n.startswith(\"/\") or n.startswith(\"\\\\\") or \"..\" in n.replace(\"\\\\\", \"/\").split(\"/\") for n in zf.namelist())","typeGuard":null,"tryCatchPattern":"from office.helpers import safe_extract\ntry:\n    safe_extract(zf, dest)\nexcept ValueError as e:\n    if \"unsafe archive entry\" in str(e):\n        quarantine(path)  # zip-slip attempt; never extract with a fallback extractor\n    else:\n        raise","preventionTips":["Pre-scan namelist() for absolute paths and '..' segments.","Extract only into a fresh, disposable temp directory.","Never replace safe_extract with raw ZipFile.extractall on untrusted input.","Quarantine and report files that trip this guard."],"tags":["security","zip-slip","extraction","ooxml","docx","pptx"],"backgroundTag":null,"analyzedSha":"f6656c1256d5a8adfa37db9110046ef20bac644c","analyzedAt":"2026-08-14T16:09:17.493Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}