All vulnerabilities
CVE-2026-56701GHSA-3446-6mgw-f79p

getgrav/grav: Grav is Vulnerable to XXE via SVG Upload

Grav getgrav/gravcomposerLibraries & packages
CVSS
6.5Medium
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 23 Jun 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.30%Chance of exploitation activity in the next 30 days, higher than 22% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in2.0.0-beta.2Fixed versionGitHub
Published23 Jun 2026Updated 8 Oct 2026CVE List

What fixes it

  • 2.0.0-beta.2getgrav/grav, composerGitHubcomposer require "getgrav/grav:^2.0.0-beta.2"
  • 2.0.0-beta.2GravCVE List
  • 2.0.0-beta.2getgrav/grav, PackagistOSVcomposer require "getgrav/grav:^2.0.0-beta.2"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Dear Grav Security Team,

A security vulnerability was discovered in Grav CMS that allows authenticated attackers to read arbitrary files from the server through XML External Entity (XXE) injection.

Vulnerability Summary

| Field | Details | |-------|---------| | Vulnerability Type | XML External Entity (XXE) Injection | | Severity | High (CVSS 7.5) | | Affected Versions | Grav CMS <= 1.7.x | | Affected Component | SVG file upload/processing | | CWE | CWE-611: Improper Restriction of XML External Entity Reference | | Authentication Required | Yes (Admin panel access) |

Technical Details

Root Cause The application uses simplexml_load_string() to process uploaded SVG files without disabling external entity loading. This allows attackers to inject XXE payloads that are processed by the XML parser.

Vulnerable Code Pattern

// Current (Vulnerable):
$svg = simplexml_load_string($content);

// No LIBXML_NOENT flag or entity loader protection

Attack Vector

  1. Attacker authenticates to Grav admin panel
  2. Uploads malicious SVG file via Pages → Media or File Manager plugin
  3. Server parses SVG and processes XXE entities
  4. Arbitrary file contents are exfiltrated

Impact

An authenticated attacker can:

  1. Read sensitive files:
  2. /etc/passwd - System user information
  3. user/accounts/*.yaml - Admin credentials and 2FA secrets
  4. user/config/system.yaml - System configuration
  5. .env files - Environment secrets and API keys
  1. Perform SSRF - Access internal services via external entity URLs
  1. Potential DoS - Billion laughs attack via recursive entity expansion

Proof of Concept

Malicious SVG Payload

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <text x="10" y="50">&xxe;</text>
</svg>

Steps to Reproduce

  1. Login to Grav CMS admin panel
  2. Navigate to Pages → select any page → Media tab
  3. Upload the malicious SVG file
  4. Observe file contents in response/error or stored output

Recommended Fix

Option 1: Add XXE Protection Flags

libxml_use_internal_errors(true);
$svg = simplexml_load_string($content, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_DTDLOAD);

Option 2: Use SVG Sanitizer Library (Recommended)

use enshrined\svgSanitize\Sanitizer;

$sanitizer = new Sanitizer();
$sanitizer->removeRemoteReferences(true);
$cleanSVG = $sanitizer->sanitize($content);

The enshrined/svg-sanitize library properly strips XXE payloads and other malicious SVG content.

Request

  1. Please acknowledge receipt of this report within 5 business days
  2. Please provide an estimated timeline for a security patch
  3. I am happy to assist with testing the fix
  4. I request a CVE be assigned for this vulnerability
  5. If you have a security advisory process, please include me in the credits

Turki Almatrafi.

---

Maintainer note — fix applied (2026-04-24)

Fixed across two repos:

  1. **Grav core on the 2.0 branch** (commit [5a12f9be8](https://github.com/getgrav/grav/commit/5a12f9be8), ships in 2.0.0-beta.2) — VectorImageMedium::__construct (the code path that reads width/height from an uploaded SVG) now strips <!DOCTYPE> and <!ENTITY> declarations before parsing, and calls simplexml_load_string with LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING. On PHP < 8 it also calls libxml_disable_entity_loader(true) for the duration of the parse.
  1. rhukster/dom-sanitizer (commit [02d08ec](https://github.com/rhukster/dom-sanitizer/commit/02d08ec)) — the library Grav ships as its SVG sanitizer. loadDocument now applies the same DOCTYPE/ENTITY strip and passes LIBXML_NONET to loadXML/loadHTML.

With both layers in place, the PoC:

<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
  <text x="10" y="50">&xxe;</text>
</svg>

no longer expands &xxe;, and the parser cannot make outbound filesystem or network requests for external entities/DTDs. Billion-laughs-style entity expansion is also neutralized because the declarations are stripped before libxml ever sees them.

Files:

  • [system/src/Grav/Common/Page/Medium/VectorImageMedium.php](https://github.com/getgrav/grav/blob/2.0/system/src/Grav/Common/Page/Medium/VectorImageMedium.php).
  • [tests/unit/Grav/Common/Security/SvgXxeSecurityTest.php](https://github.com/getgrav/grav/blob/2.0/tests/unit/Grav/Common/Security/SvgXxeSecurityTest.php) — XXE neutralization + billion-laughs + plain-SVG regression.
  • dom-sanitizer: [src/DOMSanitizer.php](https://github.com/rhukster/dom-sanitizer/blob/main/src/DOMSanitizer.php) + two new XXE tests in its own suite.

Text from GitHub Advisory Database

Affected versions 54

  • getgrav/grav, composer< 2.0.0-beta.2GitHub
  • Gravaffected: all versions before 2.0.0-beta.2CVE List
  • Gravunaffected: 2.0.0-beta.2CVE List
  • getgrav/grav, Packagistfrom first release, fixed in 2.0.0-beta.2OSV
  • getgrav/grav, Packagist0.8.0OSV
  • getgrav/grav, Packagist0.9.0OSV
  • getgrav/grav, Packagist0.9.1OSV
  • getgrav/grav, Packagist0.9.10OSV
  • getgrav/grav, Packagist0.9.11OSV
  • getgrav/grav, Packagist0.9.12OSV
  • getgrav/grav, Packagist0.9.13OSV
  • getgrav/grav, Packagist0.9.14OSV
  • getgrav/grav, Packagist0.9.15OSV
  • getgrav/grav, Packagist0.9.16OSV
  • getgrav/grav, Packagist0.9.17OSV
  • getgrav/grav, Packagist0.9.18OSV
  • getgrav/grav, Packagist0.9.19OSV
  • getgrav/grav, Packagist0.9.2OSV
  • getgrav/grav, Packagist0.9.20OSV
  • getgrav/grav, Packagist0.9.21OSV
  • getgrav/grav, Packagist0.9.22OSV
  • getgrav/grav, Packagist0.9.23OSV
  • getgrav/grav, Packagist0.9.24OSV
  • getgrav/grav, Packagist0.9.25OSV
  • getgrav/grav, Packagist0.9.26OSV
  • getgrav/grav, Packagist0.9.27OSV
  • getgrav/grav, Packagist0.9.28OSV
  • getgrav/grav, Packagist0.9.29OSV
  • getgrav/grav, Packagist0.9.3OSV
  • getgrav/grav, Packagist0.9.30OSV
  • getgrav/grav, Packagist0.9.31OSV
  • getgrav/grav, Packagist0.9.32OSV
  • getgrav/grav, Packagist0.9.33OSV
  • getgrav/grav, Packagist0.9.34OSV
  • getgrav/grav, Packagist0.9.35OSV
  • getgrav/grav, Packagist0.9.36OSV
  • getgrav/grav, Packagist0.9.37OSV
  • getgrav/grav, Packagist0.9.38OSV
  • getgrav/grav, Packagist0.9.39OSV
  • getgrav/grav, Packagist0.9.4OSV
  • getgrav/grav, Packagist0.9.40OSV
  • getgrav/grav, Packagist0.9.41OSV
  • getgrav/grav, Packagist0.9.42OSV
  • getgrav/grav, Packagist0.9.43OSV
  • getgrav/grav, Packagist0.9.44OSV
  • getgrav/grav, Packagist0.9.45OSV
  • getgrav/grav, Packagist0.9.5OSV
  • getgrav/grav, Packagist0.9.6OSV
  • getgrav/grav, Packagist0.9.7OSV
  • getgrav/grav, Packagist0.9.8OSV
  • getgrav/grav, Packagist0.9.9OSV
  • getgrav/grav, Packagist1.0.0OSV
  • getgrav/grav, Packagist1.0.0-rc.1OSV
  • getgrav/grav, Packagist1.0.0-rc.2OSV

References 5