anthropics/skills · error · ValueError
symlink archive entry not allowed: {m.filename!r}
Error message
symlink archive entry not allowed: {m.filename!r} What it means
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.
Source
Thrown at skills/docx/scripts/office/helpers/__init__.py:78
return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./")
def part_text(data: bytes) -> str:
return data.decode("utf-8", "surrogateescape")
XML_SPACE = " \t\r\n"
def rendered_text(text: str, preserve: bool) -> str:
return text if preserve else text.strip(XML_SPACE)
def safe_extract(zf: zipfile.ZipFile, dest: Path) -> None:
dest = dest.resolve()
for m in zf.infolist():
if stat.S_ISLNK(m.external_attr >> 16):
raise ValueError(f"symlink archive entry not allowed: {m.filename!r}")
target = (dest / m.filename).resolve()
if not target.is_relative_to(dest):
raise ValueError(f"unsafe archive entry: {m.filename!r}")
zf.extract(m, dest)
def rezip(src_dir: Path, out_path: Path) -> None:
files = sorted(p for p in src_dir.rglob("*") if p.is_file())
ct = src_dir / "[Content_Types].xml"
fd, tmp_name = tempfile.mkstemp(
prefix=out_path.name + ".", suffix=".tmp", dir=out_path.parent
)
tmp_out = Path(tmp_name)
try:
with os.fdopen(fd, "wb") as fh:
with zipfile.ZipFile(fh, "w", zipfile.ZIP_DEFLATED) as zf:
if ct.exists():
zf.write(ct, ct.relative_to(src_dir), compress_type=zipfile.ZIP_STORED)View on GitHub (pinned to f6656c1256)
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.
Example fix
# before (produces the guard)
with zipfile.ZipFile(uploaded) as zf:
zf.extractall(dest) # unsafe if re-implemented
# after: pre-scan, then use safe_extract
import stat
with zipfile.ZipFile(uploaded) as zf:
bad = [m.filename for m in zf.infolist() if stat.S_ISLNK(m.external_attr >> 16)]
if bad:
raise ValueError(f"quarantine: symlink entries {bad}")
safe_extract(zf, dest) Defensive patterns
Strategy: validation
Validate before calling
import stat, zipfile
def has_symlink_entries(path: str) -> bool:
with zipfile.ZipFile(path) as zf:
return any(stat.S_ISLNK(m.external_attr >> 16) for m in zf.infolist()) Try / catch
from office.helpers import safe_extract
try:
safe_extract(zf, dest)
except ValueError as e:
if "symlink archive entry" in str(e):
quarantine(path) # untrusted input; do not repair
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Processing untrusted uploaded documents; archives rebuilt with tools that preserved symlinks; penetration-test payloads targeting naive ZipFile.extract() usage.
Related errors
- unsafe archive entry: {m.filename!r}
- symlink archive entry not allowed: {m.filename!r}
- relationship target escapes the package: {target!r}
- unsafe archive entry: {m.filename!r}
- relationship target is not a POSIX part name: {target!r}
AI-assisted analysis of anthropics/skills@f6656c1256 (2026-08-14).
Data as JSON: /api/errors/eb727ad2e5336736.
Report an issue: GitHub.