CVE-2026-58657Medium· 4.8▾ SunlitGrav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav
▾ Sunlit zone — Low / medium · no exploitation signal
impact 26.4 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Sep 16.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.4%
Grav 2.0.0-rc.9 and the current 2.0 branch still allow stored CSS injection through Markdown image media actions. The prior media hardening rejects direct ?style= payloads and unsafe attribute() fallbacks, but the adjacent resize() action still writes caller-controlled values directly into styleAttributes.
A publisher who can edit page Markdown can store a crafted image URL that renders additional CSS declarations in the final <img style=...> attribute. This crosses the same lower-privileged publisher to higher-privileged reviewer/admin rendered-content boundary as the earlier media style and attribute advisories.
A lower-privileged content editor can persist CSS declarations that are rendered when a higher-privileged user views the page or admin preview. The demonstrated payload creates a full-viewport fixed overlay by injecting position:fixed, viewport dimensions, background color, and z-index declarations.
This does not require JavaScript execution. The impact is stored CSS injection in rendered content, with UI redress/overlay and content-manipulation risk in higher-privileged sessions.
Tested versions:
6582166173bb8eb5869d96aea384e0e73777c94c2.0.0-rc.9 commit e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8Minimal Markdown payload:

A minimal PHPUnit-style reproducer can drive the same parser path directly:
$m = new class {
use \Grav\Common\Media\Traits\MediaObjectTrait;
use \Grav\Common\Media\Traits\StaticResizeTrait;
public function addMetaFile($filepath) {}
public function __toString(): string { return ''; }
public function url($reset = true) { return '/img.png'; }
public function get($name, mixed $default = null, $separator = null) { return $default; }
public function set($name, mixed $value, $separator = null) { return $this; }
protected function createThumbnail($thumb) { return null; }
protected function createLink(array $attributes) { return null; }
protected function getItems(): array { return []; }
};
$excerpts = new \Grav\Common\Page\Markdown\Excerpts(null, ['markdown' => [], 'images' => []]);
$m = $excerpts->processMediaActions(
$m,
'image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200'
);
$element = $m->parsedownElement('', '', '', '', false);
var_dump($element['attributes']['style']);
Observed style attribute:
width: 100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999px;height: 200px;
The appended px lands on the final z-index value, but the preceding injected declarations remain syntactically valid CSS.
system/src/Grav/Common/Page/Markdown/Excerpts.php::processMediaActions() parses the image query string into media actions and invokes the requested public media method with call_user_func_array([$medium, $action['method']], $args).
For resize(), system/src/Grav/Common/Media/Traits/StaticResizeTrait.php::resize() stores width and height directly into style attributes:
$this->styleAttributes['width'] = $width . 'px';
$this->styleAttributes['height'] = $height . 'px';
It does not verify that the values are numeric, length-only, or free of CSS declaration delimiters. Later, system/src/Grav/Common/Media/Traits/MediaObjectTrait.php::parsedownElement() serializes keyed style attributes as raw CSS declarations:
$style .= $key . ': ' . $value . ';';
The sanitizer added for direct style() inputs is not reached for values introduced by resize(). As a result, resize=100;position:fixed;...,200 breaks out of the intended width: value and injects additional declarations.
On both current 2.0 and 2.0.0-rc.9, the targeted regression test produced the injected style string above. Existing tests still confirm the direct style() and attribute() paths are rejected; the bypass is specific to the adjacent resize() styleAttributes path.
Sanitize or type-normalize all values before they enter styleAttributes, not only values passed through MediaObjectTrait::style(). For resize(), cast or validate width and height as numeric values before appending px, or use a shared CSS declaration builder that rejects semicolons, colons, property names, and other declaration-breaking characters. Add regression coverage for resize=100;position:fixed;top:0,200 and any other media action that writes to styleAttributes directly.
getgrav/grav = 2.0.0-rc.9Upgrade to a patched release:
getgrav/grav 2.0.0Connected by shared product, vendor, weakness, or advisory.
GHSA-4wj4-79rr-pvffMedium· 4.8Duplicate Advisory: Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav
GHSA-993v-76jg-67xrMedium· 5.4Duplicate Advisory: Grav: Single invalid UTF-8 byte disables every rule in Security::detectXss(), bypassing the page-content XSS safety gate
GHSA-9pr6-8r9w-wvmjCritical· 8.7Duplicate Advisory: Grav: detectXss() misses an event-handler attribute after an unpaired quote in an unquoted attribute value, giving stored XSS
GHSA-cgvr-f65r-pjv3Medium· 5.4Duplicate Advisory: Grav: Stored XSS via quoted-attribute bypass in detectXss
GHSA-q8cg-5m48-5c25Medium· 7.6Duplicate Advisory: Grav: Stored XSS via Markdown audio/video media <source> URL
GHSA-373m-p57p-8665Medium· 6.1Duplicate Advisory: Grav: XSS Blueprint Validation Bypass via Twig String Concatenation