{"id":"CVE-2026-61690","aliases":["EUVD-2026-62534","GHSA-928x-9mpw-8h56"],"status":"published","title":"Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits","description":"Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.","severity":{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.0053,"percentile":0.45182,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-409"],"published":"2026-07-10T13:58:08.99Z","modified":"2026-10-28T02:30:53Z","fixes":[{"ecosystem":"Packagist","package":"getgrav/grav","fixed_in":["2.0.1"],"sources":["ghsa"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.0.1"}]}]},{"source":"osv","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.0.1"}]}],"versions":["0.8.0","0.9.0","0.9.1","0.9.10","0.9.11","0.9.12","0.9.13","0.9.14","0.9.15","0.9.16","0.9.17","0.9.18","0.9.19","0.9.2","0.9.20","0.9.21","0.9.22","0.9.23","0.9.24","0.9.25","0.9.26","0.9.27","0.9.28","0.9.29","0.9.3","0.9.30","0.9.31","0.9.32","0.9.33","0.9.34","0.9.35","0.9.36","0.9.37","0.9.38","0.9.39","0.9.4","0.9.40","0.9.41","0.9.42","0.9.43","0.9.44","0.9.45","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","1.0.0","1.0.0-rc.1","1.0.0-rc.2","1.0.0-rc.3","1.0.0-rc.4","1.0.0-rc.5","1.0.0-rc.6","1.0.1","1.0.10","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8","1.0.9","1.1.0","1.1.0-beta.1","1.1.0-beta.2","1.1.0-beta.3","1.1.0-beta.4","1.1.0-beta.5","1.1.0-rc.1","1.1.0-rc.2","1.1.0-rc.3","1.1.1","1.1.10","1.1.11","1.1.12","1.1.13","1.1.14","1.1.15","1.1.16","1.1.17","1.1.2","1.1.3","1.1.4","1.1.5","1.1.6","1.1.7","1.1.8","1.1.9","1.1.9-rc.1","1.1.9-rc.2","1.1.9-rc.3","1.2.0","1.2.0-rc.1","1.2.0-rc.2","1.2.0-rc.3","1.2.1","1.2.2","1.2.3","1.2.4","1.3.0","1.3.0-rc.1","1.3.0-rc.2","1.3.0-rc.3","1.3.0-rc.4","1.3.0-rc.5","1.3.1","1.3.10","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6","1.3.7","1.3.8","1.3.9","1.4.0","1.4.0-beta.1","1.4.0-beta.2","1.4.0-beta.3","1.4.0-rc.1","1.4.0-rc.2","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.5.0","1.5.0-beta.1","1.5.0-beta.2","1.5.0-rc.1","1.5.1","1.5.10","1.5.2","1.5.3","1.5.4","1.5.5","1.5.6","1.5.7","1.5.8","1.5.9","1.6.0","1.6.0-beta.1","1.6.0-beta.2","1.6.0-beta.3","1.6.0-beta.4","1.6.0-beta.5","1.6.0-beta.6","1.6.0-beta.7","1.6.0-beta.8","1.6.0-rc.1","1.6.0-rc.2","1.6.0-rc.3","1.6.0-rc.4","1.6.1","1.6.10","1.6.11","1.6.12","1.6.13","1.6.14","1.6.15","1.6.16","1.6.17","1.6.18","1.6.19","1.6.2","1.6.20","1.6.21","1.6.22","1.6.23","1.6.24","1.6.25","1.6.26","1.6.27","1.6.28","1.6.29","1.6.3","1.6.30","1.6.31","1.6.4","1.6.5","1.6.6","1.6.7","1.6.8","1.6.9","1.7.0","1.7.0-beta.1","1.7.0-beta.10","1.7.0-beta.2","1.7.0-beta.3","1.7.0-beta.4","1.7.0-beta.5","1.7.0-beta.6","1.7.0-beta.7","1.7.0-beta.8","1.7.0-beta.9","1.7.0-rc.1","1.7.0-rc.10","1.7.0-rc.11","1.7.0-rc.12","1.7.0-rc.13","1.7.0-rc.14","1.7.0-rc.15","1.7.0-rc.16","1.7.0-rc.17","1.7.0-rc.18","1.7.0-rc.19","1.7.0-rc.2","1.7.0-rc.20","1.7.0-rc.3","1.7.0-rc.4","1.7.0-rc.5","1.7.0-rc.6","1.7.0-rc.7","1.7.0-rc.8","1.7.0-rc.9","1.7.1","1.7.10","1.7.12","1.7.13","1.7.14","1.7.15","1.7.16","1.7.17","1.7.18","1.7.19","1.7.20","1.7.21","1.7.22","1.7.23","1.7.24","1.7.25","1.7.26","1.7.26.1","1.7.27","1.7.27.1","1.7.28","1.7.29","1.7.29.1","1.7.3","1.7.30","1.7.31","1.7.32","1.7.33","1.7.34","1.7.35","1.7.36","1.7.37","1.7.37.1","1.7.38","1.7.39","1.7.39.1","1.7.39.2","1.7.39.3","1.7.39.4","1.7.4","1.7.40","1.7.41","1.7.41.1","1.7.41.2","1.7.42","1.7.42.1","1.7.42.2","1.7.42.3","1.7.43","1.7.44","1.7.45","1.7.46","1.7.47","1.7.48","1.7.49","1.7.49.1","1.7.49.2","1.7.49.3","1.7.49.4","1.7.49.5","1.7.5","1.7.51","1.7.52","1.7.53","1.7.53.1","1.7.53.2","1.7.53.3","1.7.6","1.7.7","1.7.8","1.7.9","1.8.0-beta.1","1.8.0-beta.10","1.8.0-beta.11","1.8.0-beta.12","1.8.0-beta.13","1.8.0-beta.14","1.8.0-beta.15","1.8.0-beta.16","1.8.0-beta.17","1.8.0-beta.18","1.8.0-beta.19","1.8.0-beta.2","1.8.0-beta.20","1.8.0-beta.21","1.8.0-beta.22","1.8.0-beta.23","1.8.0-beta.24","1.8.0-beta.25","1.8.0-beta.26","1.8.0-beta.27","1.8.0-beta.28","1.8.0-beta.29","1.8.0-beta.3","1.8.0-beta.4","1.8.0-beta.5","1.8.0-beta.6","1.8.0-beta.7","1.8.0-beta.8","1.8.0-beta.9","2.0.0","2.0.0-beta.1","2.0.0-beta.2","2.0.0-beta.3","2.0.0-beta.4","2.0.0-rc.1","2.0.0-rc.10","2.0.0-rc.2","2.0.0-rc.3","2.0.0-rc.4","2.0.0-rc.5","2.0.0-rc.6","2.0.0-rc.7","2.0.0-rc.8","2.0.0-rc.9"],"bundled":2}],"products":[{"vendor":"getgrav","product":"grav"}],"references":[{"url":"https://github.com/getgrav/grav/security/advisories/GHSA-928x-9mpw-8h56","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/getgrav/grav/commit/1c1003cfcab5344203d6fde1aaa1f9a4ee3413ff","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/getgrav/grav/releases/tag/2.0.1","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61690","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-928x-9mpw-8h56"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61455.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-61455","tags":["advisory"]},{"url":"https://www.vulncheck.com/advisories/grav-before-decompression-bomb-via-ziparchiver","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/61xxx/CVE-2026-61690.json","tags":["advisory"]},{"url":"https://github.com/getgrav/grav","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-61690","title":"Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits","description":"Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.","status":"published","published":"2026-08-19T15:20:10.966Z","modified":"2026-08-25T02:07:51.415Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"cna"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T14:19:53.4444Z"}],"euvd":[{"id":"EUVD-2026-62534","description":"Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.","status":"published","published":"2026-08-19T15:20:10Z","modified":"2026-08-25T02:07:51Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"updated_at":"2026-11-02T18:01:33.760029Z"}],"ghsa":[{"id":"GHSA-928x-9mpw-8h56","title":"Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits","description":"## Summary\n`ZipArchiver::extract()` lacks limits on uncompressed size, file count, and nesting depth, creating a distinct, unpatched variant of the GHSA-2vcx-h8p2-9pg9 zip bomb vulnerability. While the parallel method Installer::unZip() received comprehensive limits, ZipArchiver::extract() remains unprotected, leaving a separate code path vulnerable to the same attack vector. The vulnerability is a distinct, unpatched variant of the bug described in GHSA-2vcx-h8p2-9pg9, as it affects a separate code path in the same codebase, implementing the same abstract class.\n\n---\n\n## Details\n\n**Vulnerable code** - `system/src/Grav/Common/Filesystem/ZipArchiver.php:29-58`:\n\n```php\npublic function extract($destination, ?callable $status = null)\n{\n    $zip = new ZipArchive();\n    $archive = $zip-\u003eopen($this-\u003earchive_file);\n\n    if ($archive === true) {\n        Folder::create($destination);\n\n        // Only guards against Zip Slip (path traversal)\n        for ($i = 0, $count = $zip-\u003ecount(); $i \u003c $count; $i++) {\n            $name = $zip-\u003egetNameIndex($i);\n            if ($name !== false \u0026\u0026 !$this-\u003eisSafeEntryPath($name)) {\n                $zip-\u003eclose();\n                throw new RuntimeException(...);\n            }\n        }\n\n        // Extracts EVERYTHING — no size, count, or depth limit\n        if (!$zip-\u003eextractTo($destination)) { ... }\n\n        $zip-\u003eclose();\n        return $this;\n    }\n}\n```\n\n**What's missing vs `Installer::unZip()`**:\n\n| Protection | `Installer::unZip()` | `ZipArchiver::extract()` |\n|-----------|---------------------|------------------------|\n| Zip Slip guard | ✅ | ✅ |\n| Max uncompressed size | ✅ (1 GiB) | ❌ |\n| Max file count | ✅ (50000) | ❌ |\n| Max nesting depth | ✅ (48) | ❌ |\n| Pre-extraction validation | ✅ All entries validated first | ❌ Extracts immediately |\n\n**The fix applied to Installer** (GHSA-2vcx, `Installer.php:178-269`):\n\n```php\n// GHSA-2vcx-h8p2-9pg9: bound what extractTo() will write to disk.\n$limits = $this-\u003earchiveLimits();\n$size = $count = $depth = 0;\n\nfor ($i = 0; $i \u003c $numFiles; $i++) {\n    $entryName = $zip-\u003egetNameIndex($i);\n    // Check size, count, and depth BEFORE extracting anything\n    if ($limits['maxSize'] \u003e 0) { $size += $entry['size']; }\n    if ($limits['maxDepth'] \u003e 0) { ... }\n    if ($limits['maxFiles'] \u003e 0) { $count++; }\n    // Reject if any limit exceeded\n}\n// Only now: $zip-\u003eextractTo($destination);\n```\n\nNone of this validation exists in `ZipArchiver::extract()`.\n\n**Reachability**: `ZipArchiver::extract()` is a public method on a concrete class, accessible via the `Archiver::create('zip')` factory. While no first-party Grav code currently calls `extract()` on a `ZipArchiver` instance, third-party plugins and custom code that use the `Archiver` abstraction for ZIP restoration will walk directly into this unprotected path.\n\n---\n\n## Proof of Concept\n\n### Step 1 - Create a zip bomb\n\n```bash\n# Create a 10 GB zip bomb (42 kB compressed)\npython3 -c \"\nimport zipfile, os\nz = zipfile.ZipFile('/tmp/zipbomb.zip', 'w', zipfile.ZIP_DEFLATED)\nzeros = b'\\x00' * (1024 * 1024 * 1024)  # 1 GB of zeros\nfor i in range(10):\n    z.writestr(f'file_{i}.txt', zeros)\nz.close()\n\"\nls -lh /tmp/zipbomb.zip\n# Output: 42K /tmp/zipbomb.zip  →  expands to 10 GB\n```\n\n### Step 2 - Extract via ZipArchiver\n\n```php\n$archiver = Archiver::create('zip');\n$archiver-\u003esetArchive('/tmp/zipbomb.zip');\n$archiver-\u003eextract('/tmp/extracted');  // ← no limits, fills disk\n```\n\nThe server's disk fills with 10 GB of data. If the web root shares the disk, the site becomes unavailable (DoS).\n\n---\n\n## Impact\n\nAny code path that extracts a user-supplied ZIP archive through `ZipArchiver::extract()` will write the entire archive to disk without limits. A 42 KB zip bomb can expand to fill available disk space, causing denial of service. On systems where the extraction directory shares a partition with the web root, the entire site becomes unavailable.\n\n---\n\n## Remediation\n\nApply the same `archiveLimits()` validation from `Installer::unZip()` to `ZipArchiver::extract()`:\n\n```php\npublic function extract($destination, ?callable $status = null)\n{\n    $zip = new ZipArchive();\n    $archive = $zip-\u003eopen($this-\u003earchive_file);\n\n    if ($archive === true) {\n        Folder::create($destination);\n\n        // Apply the same archive limits as Installer::unZip()\n        $limits = $this-\u003earchiveLimits();\n        $totalSize = 0;\n        $totalFiles = 0;\n\n        for ($i = 0, $count = $zip-\u003ecount(); $i \u003c $count; $i++) {\n            $name = $zip-\u003egetNameIndex($i);\n            if ($name === false) continue;\n\n            // Zip Slip guard (existing)\n            if (!$this-\u003eisSafeEntryPath($name)) {\n                $zip-\u003eclose();\n                throw new RuntimeException(...);\n            }\n\n            // Decompression bomb guards (NEW)\n            $stat = $zip-\u003estatIndex($i);\n            $totalSize += $stat['size'] ?? 0;\n            $totalFiles++;\n\n            $depth = count(explode('/', trim($name, '/')));\n            if ($limits['maxDepth'] \u003e 0 \u0026\u0026 $depth \u003e $limits['maxDepth']) {\n                $zip-\u003eclose();\n                throw new RuntimeException('Archive exceeds max nesting depth');\n            }\n        }\n\n        if ($limits['maxSize'] \u003e 0 \u0026\u0026 $totalSize \u003e $limits['maxSize']) {\n            $zip-\u003eclose();\n            throw new RuntimeException('Archive exceeds max uncompressed size');\n        }\n        if ($limits['maxFiles'] \u003e 0 \u0026\u0026 $totalFiles \u003e $limits['maxFiles']) {\n            $zip-\u003eclose();\n            throw new RuntimeException('Archive exceeds max file count');\n        }\n\n        if (!$zip-\u003eextractTo($destination)) { ... }\n        $zip-\u003eclose();\n        return $this;\n    }\n}\n```","status":"published","published":"2026-09-02T21:35:43Z","modified":"2026-09-02T21:35:45Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T14:41:11.621873Z"}],"nvd":[{"id":"CVE-2026-61690","description":"Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.","status":"published","published":"2026-08-19T16:18:16.867Z","modified":"2026-09-09T21:13:25.91Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium","assessor":"secondary"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T14:12:39.962615Z"}],"osv":[{"id":"CVE-2026-61455","title":"Grav before 2.0.1 Decompression Bomb via ZipArchiver","description":"Grav before 2.0.1 contains a decompression bomb vulnerability in ZipArchiver::extract() that lacks limits on uncompressed size, file count, and nesting depth. Attackers can supply a crafted ZIP archive that expands to fill available disk space, causing denial of service by exhausting storage resources.","status":"published","published":"2026-07-10T13:58:08.99Z","modified":"2026-10-28T02:30:53.61248584Z","cvss":[{"version":"4.0","score":7.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T15:45:12.933503Z"},{"id":"CVE-2026-61690","title":"Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits","description":"Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.","status":"published","published":"2026-08-19T15:20:10.966Z","modified":"2026-10-28T02:30:31.944012148Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T15:45:12.933503Z"},{"id":"GHSA-928x-9mpw-8h56","title":"Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits","description":"## Summary\n`ZipArchiver::extract()` lacks limits on uncompressed size, file count, and nesting depth, creating a distinct, unpatched variant of the GHSA-2vcx-h8p2-9pg9 zip bomb vulnerability. While the parallel method Installer::unZip() received comprehensive limits, ZipArchiver::extract() remains unprotected, leaving a separate code path vulnerable to the same attack vector. The vulnerability is a distinct, unpatched variant of the bug described in GHSA-2vcx-h8p2-9pg9, as it affects a separate code path in the same codebase, implementing the same abstract class.\n\n---\n\n## Details\n\n**Vulnerable code** - `system/src/Grav/Common/Filesystem/ZipArchiver.php:29-58`:\n\n```php\npublic function extract($destination, ?callable $status = null)\n{\n    $zip = new ZipArchive();\n    $archive = $zip-\u003eopen($this-\u003earchive_file);\n\n    if ($archive === true) {\n        Folder::create($destination);\n\n        // Only guards against Zip Slip (path traversal)\n        for ($i = 0, $count = $zip-\u003ecount(); $i \u003c $count; $i++) {\n            $name = $zip-\u003egetNameIndex($i);\n            if ($name !== false \u0026\u0026 !$this-\u003eisSafeEntryPath($name)) {\n                $zip-\u003eclose();\n                throw new RuntimeException(...);\n            }\n        }\n\n        // Extracts EVERYTHING — no size, count, or depth limit\n        if (!$zip-\u003eextractTo($destination)) { ... }\n\n        $zip-\u003eclose();\n        return $this;\n    }\n}\n```\n\n**What's missing vs `Installer::unZip()`**:\n\n| Protection | `Installer::unZip()` | `ZipArchiver::extract()` |\n|-----------|---------------------|------------------------|\n| Zip Slip guard | ✅ | ✅ |\n| Max uncompressed size | ✅ (1 GiB) | ❌ |\n| Max file count | ✅ (50000) | ❌ |\n| Max nesting depth | ✅ (48) | ❌ |\n| Pre-extraction validation | ✅ All entries validated first | ❌ Extracts immediately |\n\n**The fix applied to Installer** (GHSA-2vcx, `Installer.php:178-269`):\n\n```php\n// GHSA-2vcx-h8p2-9pg9: bound what extractTo() will write to disk.\n$limits = $this-\u003earchiveLimits();\n$size = $count = $depth = 0;\n\nfor ($i = 0; $i \u003c $numFiles; $i++) {\n    $entryName = $zip-\u003egetNameIndex($i);\n    // Check size, count, and depth BEFORE extracting anything\n    if ($limits['maxSize'] \u003e 0) { $size += $entry['size']; }\n    if ($limits['maxDepth'] \u003e 0) { ... }\n    if ($limits['maxFiles'] \u003e 0) { $count++; }\n    // Reject if any limit exceeded\n}\n// Only now: $zip-\u003eextractTo($destination);\n```\n\nNone of this validation exists in `ZipArchiver::extract()`.\n\n**Reachability**: `ZipArchiver::extract()` is a public method on a concrete class, accessible via the `Archiver::create('zip')` factory. While no first-party Grav code currently calls `extract()` on a `ZipArchiver` instance, third-party plugins and custom code that use the `Archiver` abstraction for ZIP restoration will walk directly into this unprotected path.\n\n---\n\n## Proof of Concept\n\n### Step 1 - Create a zip bomb\n\n```bash\n# Create a 10 GB zip bomb (42 kB compressed)\npython3 -c \"\nimport zipfile, os\nz = zipfile.ZipFile('/tmp/zipbomb.zip', 'w', zipfile.ZIP_DEFLATED)\nzeros = b'\\x00' * (1024 * 1024 * 1024)  # 1 GB of zeros\nfor i in range(10):\n    z.writestr(f'file_{i}.txt', zeros)\nz.close()\n\"\nls -lh /tmp/zipbomb.zip\n# Output: 42K /tmp/zipbomb.zip  →  expands to 10 GB\n```\n\n### Step 2 - Extract via ZipArchiver\n\n```php\n$archiver = Archiver::create('zip');\n$archiver-\u003esetArchive('/tmp/zipbomb.zip');\n$archiver-\u003eextract('/tmp/extracted');  // ← no limits, fills disk\n```\n\nThe server's disk fills with 10 GB of data. If the web root shares the disk, the site becomes unavailable (DoS).\n\n---\n\n## Impact\n\nAny code path that extracts a user-supplied ZIP archive through `ZipArchiver::extract()` will write the entire archive to disk without limits. A 42 KB zip bomb can expand to fill available disk space, causing denial of service. On systems where the extraction directory shares a partition with the web root, the entire site becomes unavailable.\n\n---\n\n## Remediation\n\nApply the same `archiveLimits()` validation from `Installer::unZip()` to `ZipArchiver::extract()`:\n\n```php\npublic function extract($destination, ?callable $status = null)\n{\n    $zip = new ZipArchive();\n    $archive = $zip-\u003eopen($this-\u003earchive_file);\n\n    if ($archive === true) {\n        Folder::create($destination);\n\n        // Apply the same archive limits as Installer::unZip()\n        $limits = $this-\u003earchiveLimits();\n        $totalSize = 0;\n        $totalFiles = 0;\n\n        for ($i = 0, $count = $zip-\u003ecount(); $i \u003c $count; $i++) {\n            $name = $zip-\u003egetNameIndex($i);\n            if ($name === false) continue;\n\n            // Zip Slip guard (existing)\n            if (!$this-\u003eisSafeEntryPath($name)) {\n                $zip-\u003eclose();\n                throw new RuntimeException(...);\n            }\n\n            // Decompression bomb guards (NEW)\n            $stat = $zip-\u003estatIndex($i);\n            $totalSize += $stat['size'] ?? 0;\n            $totalFiles++;\n\n            $depth = count(explode('/', trim($name, '/')));\n            if ($limits['maxDepth'] \u003e 0 \u0026\u0026 $depth \u003e $limits['maxDepth']) {\n                $zip-\u003eclose();\n                throw new RuntimeException('Archive exceeds max nesting depth');\n            }\n        }\n\n        if ($limits['maxSize'] \u003e 0 \u0026\u0026 $totalSize \u003e $limits['maxSize']) {\n            $zip-\u003eclose();\n            throw new RuntimeException('Archive exceeds max uncompressed size');\n        }\n        if ($limits['maxFiles'] \u003e 0 \u0026\u0026 $totalFiles \u003e $limits['maxFiles']) {\n            $zip-\u003eclose();\n            throw new RuntimeException('Archive exceeds max file count');\n        }\n\n        if (!$zip-\u003eextractTo($destination)) { ... }\n        $zip-\u003eclose();\n        return $this;\n    }\n}\n```","status":"published","published":"2026-09-02T21:35:43Z","modified":"2026-09-02T21:45:10.189136464Z","cvss":[{"version":"3.1","score":6.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H","level":"medium"}],"cwes":["CWE-409"],"updated_at":"2026-11-02T15:46:43.123867Z"}]}}
