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

  1. If the archive is trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_files: 200000
  2. Verify the entry count first: unzip -l package.zip | tail -1 (or count via ZipArchive) to see whether the number is legitimate
  3. 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
  4. 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

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


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