{"record":{"id":"0f350adc6f8a8ca2","repo":"getgrav/grav","slug":"ziparchiver-refused-to-extract-archive-file-en-0f350a","errorCode":null,"errorMessage":"ZipArchiver: refused to extract {archive_file}. Entry \"{name}\" exceeds the maximum nesting depth ({maxDepth}).","messagePattern":"ZipArchiver: refused to extract (.+?)\\. Entry \"(.+?)\" exceeds the maximum nesting depth \\((.+?)\\)\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Filesystem/ZipArchiver.php","lineNumber":73,"sourceCode":"            }\n\n            $totalSize = 0;\n            for ($i = 0; $i < $numFiles; $i++) {\n                $name = $zip->getNameIndex($i);\n                if ($name === false) {\n                    continue;\n                }\n\n                if (!$this->isSafeEntryPath($name)) {\n                    $zip->close();\n                    throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Entry \"' . $name . '\" would escape the destination directory (Zip Slip).');\n                }\n\n                if ($maxDepth > 0) {\n                    $depth = count(array_filter(preg_split('#[\\\\\\\\/]+#', trim($name, '/\\\\'))));\n                    if ($depth > $maxDepth) {\n                        $zip->close();\n                        throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Entry \"' . $name . '\" exceeds the maximum nesting depth (' . $maxDepth . ').');\n                    }\n                }\n\n                if ($maxSize > 0) {\n                    // Advisory only: statIndex()['size'] is the uncompressed size\n                    // declared in the central directory, which the archive author\n                    // controls and can forge small (GHSA-8h9x-89f2-m7x3). It gives\n                    // an early reject for honest oversized archives, but the real\n                    // enforcement happens during streamed extraction below, against\n                    // the bytes actually inflated.\n                    $stat = $zip->statIndex($i);\n                    if (is_array($stat) && isset($stat['size'])) {\n                        $totalSize += (int) $stat['size'];\n                        if ($totalSize > $maxSize) {\n                            $zip->close();\n                            throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Archive exceeds the maximum uncompressed size (' . $maxSize . ' bytes).');\n                        }\n                    }","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Filesystem/ZipArchiver.php#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the offending entry named in the message: unzip -l package.zip | awk '{print $4}' | awk -F/ 'NF>40' to see the deep chains","If legitimate, raise the cap in user/config/system.yaml: gpm: archive: max_depth: 64","If the depth is unexpected/artificial, reject the archive rather than raising the limit — deep nesting is a known attack shape","Re-package flattened (strip empty path segments) when you control the archive's creation"],"exampleFix":"# user/config/system.yaml — before (default)\ngpm:\n  archive:\n    max_depth: 48\n\n# after — allow a trusted package with deeper trees\ngpm:\n  archive:\n    max_depth: 64","handlingStrategy":"validation","validationCode":"$maxDepth = (int) Grav::instance()['config']->get('system.gpm.archive.max_depth', 48);\n$zip = new ZipArchive();\nif ($zip->open($path) === true) {\n    for ($i = 0; $i < $zip->count(); $i++) {\n        $name = (string) $zip->getNameIndex($i);\n        $depth = count(array_filter(preg_split('#[\\\\\\\\/]+#', trim($name, '/\\\\'))));\n        if ($depth > $maxDepth) {\n            $zip->close();\n            throw new RuntimeException(\"Entry {$name} nested {$depth} deep (limit {$maxDepth})\");\n        }\n    }\n    $zip->close();\n}","typeGuard":null,"tryCatchPattern":"try {\n    (new ZipArchiver($path))->extract($destination);\n} catch (RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'nesting depth')) {\n        // inspect the named entry; raise max_depth only for trusted, legitimately deep packages\n    }\n}","preventionTips":["Keep gpm.archive.max_depth documented alongside your package sources so config matches reality","When generating archives yourself, flatten empty directory chains and vendor trees","Unexpected depth in a third-party archive is suspicious — verify before raising the cap"],"tags":["grav","zip","security","cwe-674","nesting-depth","archive-limits"],"backgroundTag":"archive-nesting-depth-limit","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}