{"record":{"id":"8e592fbb0349c318","repo":"getgrav/grav","slug":"ziparchiver-refused-to-extract-archive-file-ar-8e592f","errorCode":null,"errorMessage":"ZipArchiver: refused to extract {archive_file}. Archive exceeds the maximum uncompressed size ({maxSize} bytes).","messagePattern":"ZipArchiver: refused to extract (.+?)\\. Archive exceeds the maximum uncompressed size \\((.+?) bytes\\)\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Common/Filesystem/ZipArchiver.php","lineNumber":89,"sourceCode":"                    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                    }\n                }\n            }\n\n            Folder::create($destination);\n\n            if ($maxSize > 0) {\n                // Enforce the uncompressed-size cap against bytes actually written,\n                // so a forged-small declared size cannot smuggle a bomb past the\n                // advisory pre-pass above (GHSA-8h9x-89f2-m7x3).\n                $this->extractStreamed($zip, $destination, $numFiles, $maxSize);\n            } elseif (!$zip->extractTo($destination)) {\n                $zip->close();\n                throw new RuntimeException('ZipArchiver: ZIP failed to extract ' . $this->archive_file . ' to ' . $destination);\n            }\n\n            $zip->close();","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Common/Filesystem/ZipArchiver.php#L71-L107","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Confirm the true uncompressed size out-of-band: unzip -l package.zip | tail -1","If trusted and legitimately large, raise the cap in user/config/system.yaml: gpm: archive: max_uncompressed_size: 5368709120 (5 GiB)","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","For user uploads, pre-check declared totals (see validationCode) and reject with a friendly quota message"],"exampleFix":"# user/config/system.yaml — before (default 1 GiB)\ngpm:\n  archive:\n    max_uncompressed_size: 1073741824\n\n# after — trusted package needs more headroom\ngpm:\n  archive:\n    max_uncompressed_size: 5368709120","handlingStrategy":"validation","validationCode":"$maxSize = (int) Grav::instance()['config']->get('system.gpm.archive.max_uncompressed_size', 1073741824);\n$zip = new ZipArchive();\nif ($zip->open($path) === true) {\n    $total = 0;\n    for ($i = 0; $i < $zip->count(); $i++) {\n        $stat = $zip->statIndex($i);\n        $total += (int) ($stat['size'] ?? 0);\n    }\n    $zip->close();\n    if ($total > $maxSize) {\n        throw new RuntimeException(sprintf('Archive declares %d bytes uncompressed (cap %d)', $total, $maxSize));\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    (new ZipArchiver($path))->extract($destination);\n} catch (RuntimeException $e) {\n    if (str_contains($e->getMessage(), 'maximum uncompressed size')) {\n        // honest oversized archive: raise the cap if trusted; forged-size bomb: reject outright\n    }\n}","preventionTips":["Cross-check declared size with unzip -l before raising limits — trust the OS tool over the zip metadata","Keep max_uncompressed_size at or below your actual free disk space","Remember enforcement also runs against bytes actually written, so a forged-small bomb still aborts — never treat a passed pre-pass as proof of safety"],"tags":["grav","zip","security","decompression-bomb","cwe-409","ghsa-8h9x-89f2-m7x3"],"backgroundTag":"zip-bomb-detected","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}