getgrav/grav · critical · RuntimeException
ZipArchiver: refused to extract {archive_file}. Entry "{name
Error message
ZipArchiver: refused to extract {archive_file}. Entry "{name}" would escape the destination directory (Zip Slip). What it means
Before extraction, ZipArchiver::extract() runs isSafeEntryPath() on every entry name to block Zip Slip (CWE-22): entries whose path resolves outside the destination — '../' sequences that climb past the root, absolute paths starting with '/', or Windows drive letters like 'C:' — cause an immediate RuntimeException naming the offending entry, and nothing is extracted. This guards against archives that would overwrite system files (e.g. ../../public/index.php).
Source
Thrown at system/src/Grav/Common/Filesystem/ZipArchiver.php:66
// GHSA-928x-9mpw-8h56).
[$maxSize, $maxFiles, $maxDepth] = $this->archiveLimits();
$numFiles = $zip->count();
if ($maxFiles > 0 && $numFiles > $maxFiles) {
$zip->close();
throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Archive exceeds the maximum file count (' . $maxFiles . ').');
}
$totalSize = 0;
for ($i = 0; $i < $numFiles; $i++) {
$name = $zip->getNameIndex($i);
if ($name === false) {
continue;
}
if (!$this->isSafeEntryPath($name)) {
$zip->close();
throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Entry "' . $name . '" would escape the destination directory (Zip Slip).');
}
if ($maxDepth > 0) {
$depth = count(array_filter(preg_split('#[\\\\/]+#', trim($name, '/\\'))));
if ($depth > $maxDepth) {
$zip->close();
throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Entry "' . $name . '" exceeds the maximum nesting depth (' . $maxDepth . ').');
}
}
if ($maxSize > 0) {
// Advisory only: statIndex()['size'] is the uncompressed size
// declared in the central directory, which the archive author
// controls and can forge small (GHSA-8h9x-89f2-m7x3). It gives
// an early reject for honest oversized archives, but the real
// enforcement happens during streamed extraction below, against
// the bytes actually inflated.
$stat = $zip->statIndex($i);View on GitHub (pinned to 6040efed04)
Solutions
- Treat this throw as a security stop, not a bug: do not bypass it — quarantine the archive and find where it came from
- List entries to confirm: unzip -l package.zip | grep -E '(^|/)\.\./|^/|^[A-Za-z]:'
- If the content is genuinely needed, extract it on an isolated machine with a tool that sanitizes paths, re-zip it with clean relative names, and reinstall
- Keep ZipArchiver/GPM up to date so the isSafeEntryPath checks stay current
Example fix
# before — installing an untrusted archive
$archiver = Archiver::create('zip')->setArchive($uploadedZip)->extract( GRAV_ROOT . '/user/themes/x');
# throws: Entry "../../index.php" would escape the destination directory (Zip Slip)
# after — validate source and entries before touching the site
# 1. only install packages from getgrav.org / trusted vendors
# 2. pre-scan entries (see validationCode) and reject any traversal pattern Defensive patterns
Strategy: validation
Validate before calling
$zip = new ZipArchive();
if ($zip->open($path) === true) {
for ($i = 0; $i < $zip->count(); $i++) {
$name = str_replace('\\', '/', (string) $zip->getNameIndex($i));
if (str_starts_with($name, '/') || preg_match('#^[a-zA-Z]:#', $name) || str_contains($name, '../')) {
$zip->close();
throw new RuntimeException('Unsafe entry path: ' . $name);
}
}
$zip->close();
} Try / catch
try {
(new ZipArchiver($path))->extract($destination);
} catch (RuntimeException $e) {
if (str_contains($e->getMessage(), 'Zip Slip')) {
// hostile or malformed archive — quarantine it and alert; never widen the check
}
} Prevention
- Only install packages from trusted origins (official GPM, verified vendors)
- Pre-scan uploaded zips for ../, absolute paths, and drive letters before any extraction
- Log and retain rejected archives for forensics — a Zip Slip hit usually means a targeted upload
When it happens
Trigger: Extracting a crafted archive containing entries like '../../evil.php', '/etc/passwd' style absolute names, or 'C:\\x\\y'; archives produced by buggy tools that store absolute or backslash-absolute paths; penetration tests or real attacks delivering malicious theme/plugin zips through an upload/install endpoint.
Common situations: Package installed from an untrusted mirror or intercepted download; custom code letting users upload and extract zips; legacy archives created with tools that recorded absolute filenames (some old Windows zippers do).
Related errors
- Backup location not allowed (outside site root): {$backup_ro
- ZipArchiver: refused to extract {archive_file}. Archive exce
- ZipArchiver: refused to extract {archive_file}. Entry "{name
- ZipArchiver: refused to extract {archive_file}. Archive exce
- ZipArchiver: ZIP failed to extract {archive_file} to {destin
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/aca57c5082adc618.
Report an issue: GitHub.