{"record":{"id":"93e955991d0e299e","repo":"anthropics/skills","slug":"unsafe-archive-entry-m-filename-r-93e955","errorCode":null,"errorMessage":"unsafe archive entry: {m.filename!r}","messagePattern":"unsafe archive entry: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/xlsx/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/xlsx/scripts/office/helpers/__init__.py#L63-L99","documentation":"safe_extract() zip-slip guard: after resolving dest/filename against the resolved destination, any entry whose real path falls outside dest is rejected. This blocks names like '../../etc/cron.d/x' or absolute-ish paths that would write outside the extraction directory.","triggerScenarios":"Extracting an archive containing '../evil.txt', '....//evil', or entries whose resolved path escapes dest (symlinked parents inside the archive contribute too, though symlinks are rejected first by the check above); calling safe_extract on a hostile or corrupt zip.","commonSituations":"Untrusted uploads processed server-side; archives renamed from .zip to .xlsx to slip past type checks; path manipulation bugs in producers writing absolute names like '/etc/passwd' as entry names.","solutions":["List entry names before extracting: `python -c \"import zipfile; [print(i.filename) for i in zipfile.ZipFile('f.xlsx').infolist()]\"` — look for leading / or .. segments","Treat it as malicious input: reject the file and alert, do not sanitize-and-continue","Keep using safe_extract (never raw zf.extractall) for anything from outside your trust boundary"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef archive_entries_are_safe(zf, dest: Path) -> bool:\n    dest = dest.resolve()\n    return all((dest / m.filename).resolve().is_relative_to(dest) for m in zf.infolist())","typeGuard":null,"tryCatchPattern":"try:\n    safe_extract(zf, dest)\nexcept ValueError as e:\n    if \"unsafe archive entry\" in str(e):\n        log.security(\"zip-slip attempt: %s\", e)\n        reject(file_path)\n    raise","preventionTips":["Always extract through safe_extract, never zf.extractall, for untrusted files","Scan entry names for leading '/', '..' and drive letters at upload time","Extract to a fresh, throwaway directory per file so escapes are also caught by OS permissions"],"tags":["zip-slip","security","zip","extraction","path-traversal"],"backgroundTag":null,"analyzedSha":"f6656c1256d5a8adfa37db9110046ef20bac644c","analyzedAt":"2026-08-14T16:09:17.493Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}