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

  1. Confirm the true uncompressed size out-of-band: unzip -l package.zip | tail -1
  2. If trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_uncompressed_size: 5368709120 (5 GiB)
  3. 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
  4. 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

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


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/8e592fbb0349c318. Report an issue: GitHub.