anthropics/skills · error · ValueError
unsafe archive entry: {m.filename!r}
Error message
unsafe archive entry: {m.filename!r} What it means
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.
Source
Thrown at skills/xlsx/scripts/office/helpers/__init__.py:81
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)
for f in files:
if f == ct:
continueView on GitHub (pinned to f6656c1256)
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
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def archive_entries_are_safe(zf, dest: Path) -> bool:
dest = dest.resolve()
return all((dest / m.filename).resolve().is_relative_to(dest) for m in zf.infolist()) Try / catch
try:
safe_extract(zf, dest)
except ValueError as e:
if "unsafe archive entry" in str(e):
log.security("zip-slip attempt: %s", e)
reject(file_path)
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- symlink archive entry not allowed: {m.filename!r}
- unsafe archive entry: {m.filename!r}
- symlink archive entry not allowed: {m.filename!r}
- unsafe archive entry: {m.filename!r}
- symlink archive entry not allowed: {m.filename!r}
AI-assisted analysis of anthropics/skills@f6656c1256 (2026-08-14).
Data as JSON: /api/errors/93e955991d0e299e.
Report an issue: GitHub.