{"record":{"id":"eb727ad2e5336736","repo":"anthropics/skills","slug":"symlink-archive-entry-not-allowed-m-filename-r","errorCode":null,"errorMessage":"symlink archive entry not allowed: {m.filename!r}","messagePattern":"symlink archive entry not allowed: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/docx/scripts/office/helpers/__init__.py","lineNumber":78,"sourceCode":"    return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(\".rels\")]).lstrip(\"./\")\n\n\ndef 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)","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/anthropics/skills/blob/f6656c1256d5a8adfa37db9110046ef20bac644c/skills/docx/scripts/office/helpers/__init__.py#L60-L96","documentation":"safe_extract() refuses to extract any archive member whose Unix mode bits (external_attr >> 16) indicate a symbolic link. Extracting symlinks from an OOXML zip would allow a crafted archive to place links that redirect later writes outside the destination — a zip-slip variant — so extraction aborts before writing anything for that entry. Present in both docx and pptx office/helpers.","triggerScenarios":"Unpacking a DOCX (or any zip passed to the unpack path) that contains an entry stored with S_IFLNK mode, e.g. 'word/document.xml -> /etc/passwd'. Common with deliberately crafted or fuzzed archives; standard Office files never contain symlink entries.","commonSituations":"Processing untrusted uploaded documents; archives rebuilt with tools that preserved symlinks; penetration-test payloads targeting naive ZipFile.extract() usage.","solutions":["Treat the file as untrusted: reject/quarantine it — the guard is doing its job.","If you control the archive, rebuild it without symlinks: unzip normally (with symlink conversion off), then rezip the plain files.","Scan the package before processing: for m in ZipFile(f).infolist(): reject if stat.S_ISLNK(m.external_attr >> 16).","Determine which upstream producer created the symlink entry and fix that pipeline."],"exampleFix":"# before (produces the guard)\nwith zipfile.ZipFile(uploaded) as zf:\n    zf.extractall(dest)  # unsafe if re-implemented\n\n# after: pre-scan, then use safe_extract\nimport stat\nwith zipfile.ZipFile(uploaded) as zf:\n    bad = [m.filename for m in zf.infolist() if stat.S_ISLNK(m.external_attr >> 16)]\n    if bad:\n        raise ValueError(f\"quarantine: symlink entries {bad}\")\n    safe_extract(zf, dest)","handlingStrategy":"validation","validationCode":"import stat, zipfile\n\ndef has_symlink_entries(path: str) -> bool:\n    with zipfile.ZipFile(path) as zf:\n        return any(stat.S_ISLNK(m.external_attr >> 16) for m in zf.infolist())","typeGuard":null,"tryCatchPattern":"from office.helpers import safe_extract\ntry:\n    safe_extract(zf, dest)\nexcept ValueError as e:\n    if \"symlink archive entry\" in str(e):\n        quarantine(path)  # untrusted input; do not repair\n    else:\n        raise","preventionTips":["Pre-scan archives for symlink entries before any processing.","Only feed trusted Office files to the pipeline.","Quarantine on guard failure — these guards indicate hostile or broken input.","Keep extraction destinations ephemeral (temp dirs) so even a miss is contained."],"tags":["security","zip-slip","symlink","ooxml","docx","pptx","extraction"],"backgroundTag":null,"analyzedSha":"f6656c1256d5a8adfa37db9110046ef20bac644c","analyzedAt":"2026-08-14T16:09:17.493Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}