getgrav/grav · error · RuntimeException
ZipArchiver: refused to extract {archive_file}. Archive exce
Error message
ZipArchiver: refused to extract {archive_file}. Archive exceeds the maximum uncompressed size ({maxSize} bytes). What it means
ZipArchiver::extract() enforces a total uncompressed-size cap (system.gpm.archive.max_uncompressed_size, default 1 GiB) in two stages. The throw at this site is the advisory pre-pass: it sums the sizes declared in the central directory (statIndex) and rejects early if the declared total exceeds the cap. Because those declared sizes are attacker-controlled and can be forged small (GHSA-8h9x-89f2-m7x3), the same message is also thrown later during streamed extraction when the bytes actually inflated exceed the cap — the pre-pass catches honest oversized archives cheaply, the stream enforces the rest.
Source
Thrown at system/src/Grav/Common/Filesystem/ZipArchiver.php:89
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);
if (is_array($stat) && isset($stat['size'])) {
$totalSize += (int) $stat['size'];
if ($totalSize > $maxSize) {
$zip->close();
throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Archive exceeds the maximum uncompressed size (' . $maxSize . ' bytes).');
}
}
}
}
Folder::create($destination);
if ($maxSize > 0) {
// Enforce the uncompressed-size cap against bytes actually written,
// so a forged-small declared size cannot smuggle a bomb past the
// advisory pre-pass above (GHSA-8h9x-89f2-m7x3).
$this->extractStreamed($zip, $destination, $numFiles, $maxSize);
} elseif (!$zip->extractTo($destination)) {
$zip->close();
throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);
}
$zip->close();View on GitHub (pinned to 6040efed04)
Solutions
- Confirm the true uncompressed size out-of-band: unzip -l package.zip | tail -1
- If trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_uncompressed_size: 5368709120 (5 GiB)
- If the size is unexpected, stop — a small zip claiming huge or lying sizes is a bomb (CWE-409); re-download from a trusted source instead of raising limits
- For user uploads, pre-check declared totals (see validationCode) and reject with a friendly quota message
Example fix
# user/config/system.yaml — before (default 1 GiB)
gpm:
archive:
max_uncompressed_size: 1073741824
# after — trusted package needs more headroom
gpm:
archive:
max_uncompressed_size: 5368709120 Defensive patterns
Strategy: validation
Validate before calling
$maxSize = (int) Grav::instance()['config']->get('system.gpm.archive.max_uncompressed_size', 1073741824);
$zip = new ZipArchive();
if ($zip->open($path) === true) {
$total = 0;
for ($i = 0; $i < $zip->count(); $i++) {
$stat = $zip->statIndex($i);
$total += (int) ($stat['size'] ?? 0);
}
$zip->close();
if ($total > $maxSize) {
throw new RuntimeException(sprintf('Archive declares %d bytes uncompressed (cap %d)', $total, $maxSize));
}
} Try / catch
try {
(new ZipArchiver($path))->extract($destination);
} catch (RuntimeException $e) {
if (str_contains($e->getMessage(), 'maximum uncompressed size')) {
// honest oversized archive: raise the cap if trusted; forged-size bomb: reject outright
}
} Prevention
- Cross-check declared size with unzip -l before raising limits — trust the OS tool over the zip metadata
- Keep max_uncompressed_size at or below your actual free disk space
- Remember enforcement also runs against bytes actually written, so a forged-small bomb still aborts — never treat a passed pre-pass as proof of safety
When it happens
Trigger: Installing a large but legitimate package (big skeletons, video-heavy themes) whose uncompressed total exceeds 1 GiB; a decompression bomb whose declared sizes alone exceed the cap; a bomb with forged-small declared sizes that trips the same message mid-extraction instead; max_uncompressed_size lowered in config below real package sizes.
Common situations: Skeleton or theme installs with large media assets; shared hosting with tight disk quotas where the 1 GiB default is deliberately reduced; untrusted zip uploads routed through Grav's archiver.
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/8e592fbb0349c318.
Report an issue: GitHub.