CVE-2026-58657: Grav - Stored CSS Injection via Markdown Image resize() Action
Summary
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.
Impact
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.
Reproduction
Tested versions:
- Grav 2.0 branch commit 6582166173bb8eb5869d96aea384e0e73777c94c - Grav 2.0.0-rc.9 commit e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8
Minimal Markdown payload:
markdown !logo
A minimal PHPUnit-style reproducer can drive the same parser path directly:
php $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); vardump($element['attributes']['style']);
Observed style attribute:
text 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.
Root Cause / Technical Details
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 calluserfuncarray([$medium, $action['method']], $args).
For resize(), system/src/Grav/Common/Media/Traits/StaticResizeTrait.php::resize() stores width and height directly into style attributes:
php $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:
php $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.
PoC Evidence
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.
Remediation
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.
Other sources
Grav before 2.0.0 (affected through 2.0.0-rc.9 and the 2.0 branch) contains a stored CSS injection vulnerability in the Markdown image resize() media action. Prior media hardening rejects direct ?style= payloads and unsafe attribute() fallbacks, but the resize() action in Excerpts::processMediaActions() writes caller-controlled values directly into the image's styleAttributes. A lower-privileged content editor who can edit page Markdown can store a crafted image URL with semicolon-delimited CSS declarations in the resize parameters, which are rendered into the final <img style=...> attribute when a higher-privileged reviewer/admin views the page or preview. This does not require JavaScript execution but enables UI redress/overlay and content-manipulation attacks (e.g., a full-viewport fixed overlay). Fixed in 2.0.0.
— NVD
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/getgrav/gravto a version that resolves this vulnerability.Fixed in 2.0.0 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 2.0.0 - Configuration
In system/src/Grav/Common/Media/Traits/StaticResizeTrait.php::resize(), sanitize/type-normalize resize width and height inputs (e.g., cast/validate as numeric and only then append 'px'), or build CSS declarations with a helper that rejects CSS declaration delimiters (e.g., semicolons), colons, property names, and other declaration-breaking characters, so values cannot inject additional style declarations into styleAttributes.
Grav Markdown image resize() action (Excerpts::processMediaActions() -> StaticResizeTrait.php::resize()) type/validation of resize width/height before writing to image styleAttributes = Cast or validate width and height as numeric values before appending 'px' (or use a shared CSS declaration builder that rejects semicolons/colon/property-name and other declaration-breaking characters)
Event History
Frequently Asked Questions
What is the severity of CVE-2026-58657?
The severity of CVE-2026-58657 is medium with a score of 4.8.
How do I fix CVE-2026-58657?
To fix CVE-2026-58657, upgrade Grav to version 2.0.0 or later.
What type of vulnerability is CVE-2026-58657?
CVE-2026-58657 is a stored CSS injection vulnerability.
What media action is affected by CVE-2026-58657?
The resize() media action in the Markdown processing feature of Grav is affected by CVE-2026-58657.
What is the impact of exploiting CVE-2026-58657?
Exploiting CVE-2026-58657 could allow an attacker to inject malicious CSS into a user's browser.