getgrav/grav · error · RuntimeException

ZipArchiver: refused to extract {archive_file}. Entry "{name

Error message

ZipArchiver: refused to extract {archive_file}. Entry "{name}" exceeds the maximum nesting depth ({maxDepth}).

What it means

As part of the pre-extraction safety pass, ZipArchiver::extract() counts path segments of each entry (splitting on slashes/backslashes) and rejects archives containing entries nested deeper than the configured maximum (system.gpm.archive.max_depth, default 48; 0 disables) with RuntimeException naming the entry and limit. Deeply nested trees are blocked because recursive cleanup and traversal can overflow the stack (CWE-674) — the same class of defense as the file-count and size caps.

Source

Thrown at system/src/Grav/Common/Filesystem/ZipArchiver.php:73

            }

            $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);
                    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).');
                        }
                    }

View on GitHub (pinned to 6040efed04)

Solutions

  1. Inspect the offending entry named in the message: unzip -l package.zip | awk '{print $4}' | awk -F/ 'NF>40' to see the deep chains
  2. If legitimate, raise the cap in user/config/system.yaml: gpm: archive: max_depth: 64
  3. If the depth is unexpected/artificial, reject the archive rather than raising the limit — deep nesting is a known attack shape
  4. Re-package flattened (strip empty path segments) when you control the archive's creation

Example fix

# user/config/system.yaml — before (default)
gpm:
  archive:
    max_depth: 48

# after — allow a trusted package with deeper trees
gpm:
  archive:
    max_depth: 64
Defensive patterns

Strategy: validation

Validate before calling

$maxDepth = (int) Grav::instance()['config']->get('system.gpm.archive.max_depth', 48);
$zip = new ZipArchive();
if ($zip->open($path) === true) {
    for ($i = 0; $i < $zip->count(); $i++) {
        $name = (string) $zip->getNameIndex($i);
        $depth = count(array_filter(preg_split('#[\\\\/]+#', trim($name, '/\\'))));
        if ($depth > $maxDepth) {
            $zip->close();
            throw new RuntimeException("Entry {$name} nested {$depth} deep (limit {$maxDepth})");
        }
    }
    $zip->close();
}

Try / catch

try {
    (new ZipArchiver($path))->extract($destination);
} catch (RuntimeException $e) {
    if (str_contains($e->getMessage(), 'nesting depth')) {
        // inspect the named entry; raise max_depth only for trusted, legitimately deep packages
    }
}

Prevention

When it happens

Trigger: Extracting a package that legitimately contains very deep trees (nested vendor or node_modules chains, generated asset folders) beyond 48 levels; a crafted archive with artificially deep paths aimed at recursive-delete overflow; max_depth lowered in config below what your packages contain.

Common situations: Installing skeletons/plugins that bundle dependency trees; build pipelines that generated absurdly deep output paths; security tightening that set a lower max_depth than real packages need.

Related errors


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