CVE-2026-58657
Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav
Summary
## 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 ` ` 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  ``` 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); var_dump($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 `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: ```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 prod
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Packagist | getgrav/grav | — | 2.0.0 |
Remediation: Upgrade to 2.0.0 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.
EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.