getgrav/grav · error · RuntimeException
ZipArchiver: refused to extract {archive_file}. Archive exce
Error message
ZipArchiver: refused to extract {archive_file}. Archive exceeds the maximum file count ({maxFiles}). What it means
ZipArchiver::extract() pre-validates every archive before extraction as a decompression-bomb defense (CWE-409), matching the caps GPM\Installer::unZip() enforces (GHSA-2vcx-h8p2-9pg9, GHSA-928x-9mpw-8h56). It counts entries with $zip->count() and, when the count exceeds the configured maximum (system.gpm.archive.max_files, default 50000; 0 disables), closes the archive and throws before anything is written. A limit of 50000 entries covers legitimate Grav packages while blocking inode-exhaustion attacks.
Source
Thrown at system/src/Grav/Common/Filesystem/ZipArchiver.php:54
// Validate every entry before creating the destination or extracting
// anything, so a bad archive leaves nothing on disk. Two guards run
// in this single pass:
//
// - Zip Slip: reject any entry whose path resolves outside the
// destination directory (e.g. "../../evil.php"). CWE-22.
// - Decompression bomb: ZipArchive::extractTo applies no limit on
// total uncompressed size, entry count, or directory depth, so a
// crafted archive can fill the disk / exhaust inodes (CWE-409) or
// nest deeply enough to overflow recursive cleanup (CWE-674).
// Reject anything over the configured limits, matching the caps
// GPM\Installer::unZip() already enforces (GHSA-2vcx-h8p2-9pg9,
// 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();View on GitHub (pinned to 6040efed04)
Solutions
- If the archive is trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_files: 200000
- Verify the entry count first: unzip -l package.zip | tail -1 (or count via ZipArchive) to see whether the number is legitimate
- If unexpected, treat the archive as hostile — do not raise limits; inspect it on an isolated machine and re-obtain the package from a trusted source
- For user uploads, pre-check counts (see validationCode) and reject oversized ones with a friendly message
Example fix
# user/config/system.yaml — before (defaults)
gpm:
archive:
max_files: 50000
# after — accommodate a trusted, legitimately huge package
gpm:
archive:
max_files: 200000
# (set 0 to disable the check entirely — not recommended for untrusted input) Defensive patterns
Strategy: validation
Validate before calling
$zip = new ZipArchive();
if ($zip->open($path) === true) {
$maxFiles = (int) Grav::instance()['config']->get('system.gpm.archive.max_files', 50000);
if ($maxFiles > 0 && $zip->count() > $maxFiles) {
$zip->close();
throw new RuntimeException(sprintf('Archive has %d entries (limit %d)', $zip->count(), $maxFiles));
}
$zip->close();
} Try / catch
try {
(new ZipArchiver($path))->extract($destination);
} catch (RuntimeException $e) {
if (str_contains($e->getMessage(), 'maximum file count')) {
// raise system.gpm.archive.max_files only if the archive is trusted; otherwise reject
}
} Prevention
- Check unzip -l | tail -1 counts before installing unusual packages
- Set gpm.archive.max_files explicitly in config when you install legitimately huge packages, so defaults do not surprise you
- Never disable limits (0) on endpoints that accept user-uploaded archives
When it happens
Trigger: Extracting a package/skeleton with tens of thousands of files (e.g. one bundling vendor trees or image sets) past the 50000 default; a crafted archive whose central directory declares a huge entry count; system.gpm.archive.max_files lowered in user config so normal packages now trip it.
Common situations: Installing a large skeleton or a plugin that ships node_modules-like trees; admins tightening archive limits for security and forgetting their own packages; importing a user-uploaded zip through custom code that routes through ZipArchiver.
Related errors
- ZipArchiver: refused to extract {archive_file}. Archive exce
- ZipArchiver: refused to extract {archive_file}. Entry "{name
- ZipArchiver: refused to extract {archive_file}. Entry "{name
- Backup location not allowed (outside site root): {$backup_ro
- ZipArchiver: ZIP failed to extract {archive_file} to {destin
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/0621f39096f13c06.
Report an issue: GitHub.