{"record":{"id":"0621f39096f13c06","repo":"getgrav/grav","slug":"ziparchiver-refused-to-extract-archive-file-ar","errorCode":null,"errorMessage":"ZipArchiver: refused to extract {archive_file}. Archive exceeds the maximum file count ({maxFiles}).","messagePattern":"ZipArchiver: refused to extract (.+?)\\. Archive exceeds the maximum file count \\((.+?)\\)\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Filesystem/ZipArchiver.php","lineNumber":54,"sourceCode":"            // Validate every entry before creating the destination or extracting\n            // anything, so a bad archive leaves nothing on disk. Two guards run\n            // in this single pass:\n            //\n            //  - Zip Slip: reject any entry whose path resolves outside the\n            //    destination directory (e.g. \"../../evil.php\"). CWE-22.\n            //  - Decompression bomb: ZipArchive::extractTo applies no limit on\n            //    total uncompressed size, entry count, or directory depth, so a\n            //    crafted archive can fill the disk / exhaust inodes (CWE-409) or\n            //    nest deeply enough to overflow recursive cleanup (CWE-674).\n            //    Reject anything over the configured limits, matching the caps\n            //    GPM\\Installer::unZip() already enforces (GHSA-2vcx-h8p2-9pg9,\n            //    GHSA-928x-9mpw-8h56).\n            [$maxSize, $maxFiles, $maxDepth] = $this->archiveLimits();\n            $numFiles = $zip->count();\n\n            if ($maxFiles > 0 && $numFiles > $maxFiles) {\n                $zip->close();\n                throw new RuntimeException('ZipArchiver: refused to extract ' . $this->archive_file . '. Archive exceeds the maximum file count (' . $maxFiles . ').');\n            }\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();","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Filesystem/ZipArchiver.php#L36-L72","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["If the archive is trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_files: 200000","Verify the entry count first: unzip -l package.zip | tail -1 (or count via ZipArchive) to see whether the number is legitimate","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","For user uploads, pre-check counts (see validationCode) and reject oversized ones with a friendly message"],"exampleFix":"# user/config/system.yaml — before (defaults)\ngpm:\n  archive:\n    max_files: 50000\n\n# after — accommodate a trusted, legitimately huge package\ngpm:\n  archive:\n    max_files: 200000\n# (set 0 to disable the check entirely — not recommended for untrusted input)","handlingStrategy":"validation","validationCode":"$zip = new ZipArchive();\nif ($zip->open($path) === true) {\n    $maxFiles = (int) Grav::instance()['config']->get('system.gpm.archive.max_files', 50000);\n    if ($maxFiles > 0 && $zip->count() > $maxFiles) {\n        $zip->close();\n        throw new RuntimeException(sprintf('Archive has %d entries (limit %d)', $zip->count(), $maxFiles));\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(), 'maximum file count')) {\n        // raise system.gpm.archive.max_files only if the archive is trusted; otherwise reject\n    }\n}","preventionTips":["Check unzip -l | tail -1 counts before installing unusual packages","Set gpm.archive.max_files explicitly in config when you install legitimately huge packages, so defaults do not surprise you","Never disable limits (0) on endpoints that accept user-uploaded archives"],"tags":["grav","zip","security","decompression-bomb","cwe-409","archive-limits"],"backgroundTag":"archive-entry-count-limit","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}