{"id":"CVE-2026-58657","aliases":["EUVD-2026-42267","GHSA-4wj4-79rr-pvff","GHSA-ffmg-hfvg-jhg9"],"status":"published","title":"Grav - Stored CSS Injection via Markdown Image resize() Action","description":"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 \u003cimg style=...\u003e 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.","severity":{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00267,"percentile":0.18911,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-07-08T13:49:12Z","modified":"2026-10-28T02:30:29Z","fixes":[{"ecosystem":"Packagist","package":"getgrav/grav","fixed_in":["2.0.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0-rc.9"},{"last_affected":"2.0.0-rc.9"},{"fixed":"2.0.0"}]}],"versions":["2.0.0-rc.9"]},{"source":"osv","ecosystem":"Packagist","package":"getgrav/grav","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.0.0-rc.9"},{"fixed":"2.0.0"}]}],"versions":["2.0.0-rc.10","2.0.0-rc.9"]}],"products":[{"vendor":"getgrav","product":"grav"},{"vendor":"grav","product":"grav"}],"references":[{"url":"https://github.com/getgrav/grav/security/advisories/GHSA-ffmg-hfvg-jhg9","tags":["vendor-advisory","exploit","advisory","web"]},{"url":"https://github.com/getgrav/grav/commit/6582166173bb8eb5869d96aea384e0e73777c94c","tags":["patch","fix","web"]},{"url":"https://github.com/getgrav/grav/commit/e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8","tags":["patch","fix","web"]},{"url":"https://www.vulncheck.com/advisories/grav-stored-css-injection-via-markdown-image-resize-action","tags":["third-party-advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-58657","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-4wj4-79rr-pvff"},{"url":"https://github.com/advisories/GHSA-ffmg-hfvg-jhg9"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/58xxx/CVE-2026-58657.json","tags":["advisory"]},{"url":"https://github.com/getgrav/grav","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-58657","title":"Grav - Stored CSS Injection via Markdown Image resize() Action","description":"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 \u003cimg style=...\u003e 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.","status":"published","published":"2026-07-08T13:49:12.183Z","modified":"2026-10-08T15:23:30.638Z","cvss":[{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna"},{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:47:28.691429Z"}],"euvd":[{"id":"EUVD-2026-42267","description":"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 \u003cimg style=...\u003e 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.","status":"published","published":"2026-07-08T13:49:12Z","modified":"2026-10-08T15:23:30Z","cvss":[{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"updated_at":"2026-11-02T17:27:31.696571Z"}],"ghsa":[{"id":"GHSA-4wj4-79rr-pvff","title":"Duplicate Advisory: Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav","description":"### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-ffmg-hfvg-jhg9. This link is maintained to preserve external references.\n\n### Original Description\nGrav 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 \u003cimg style=...\u003e 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.","status":"withdrawn","published":"2026-07-08T15:32:01Z","modified":"2026-09-16T22:14:38Z","cvss":[{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium"},{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:41:17.136201Z"},{"id":"GHSA-ffmg-hfvg-jhg9","title":"Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav","description":"## Summary\n\nGrav 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`.\n\nA publisher who can edit page Markdown can store a crafted image URL that renders additional CSS declarations in the final `\u003cimg style=...\u003e` attribute. This crosses the same lower-privileged publisher to higher-privileged reviewer/admin rendered-content boundary as the earlier media style and attribute advisories.\n\n## Impact\n\nA 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.\n\nThis 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.\n\n## Reproduction\n\nTested versions:\n\n- Grav 2.0 branch commit `6582166173bb8eb5869d96aea384e0e73777c94c`\n- Grav `2.0.0-rc.9` commit `e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8`\n\nMinimal Markdown payload:\n\n```markdown\n![logo](image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200)\n```\n\nA minimal PHPUnit-style reproducer can drive the same parser path directly:\n\n```php\n$m = new class {\n    use \\Grav\\Common\\Media\\Traits\\MediaObjectTrait;\n    use \\Grav\\Common\\Media\\Traits\\StaticResizeTrait;\n\n    public function addMetaFile($filepath) {}\n    public function __toString(): string { return ''; }\n    public function url($reset = true) { return '/img.png'; }\n    public function get($name, mixed $default = null, $separator = null) { return $default; }\n    public function set($name, mixed $value, $separator = null) { return $this; }\n    protected function createThumbnail($thumb) { return null; }\n    protected function createLink(array $attributes) { return null; }\n    protected function getItems(): array { return []; }\n};\n\n$excerpts = new \\Grav\\Common\\Page\\Markdown\\Excerpts(null, ['markdown' =\u003e [], 'images' =\u003e []]);\n$m = $excerpts-\u003eprocessMediaActions(\n    $m,\n    'image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200'\n);\n$element = $m-\u003eparsedownElement('', '', '', '', false);\nvar_dump($element['attributes']['style']);\n```\n\nObserved style attribute:\n\n```text\nwidth: 100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999px;height: 200px;\n```\n\nThe appended `px` lands on the final `z-index` value, but the preceding injected declarations remain syntactically valid CSS.\n\n## Root Cause / Technical Details\n\n`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)`.\n\nFor `resize()`, `system/src/Grav/Common/Media/Traits/StaticResizeTrait.php::resize()` stores width and height directly into style attributes:\n\n```php\n$this-\u003estyleAttributes['width'] = $width . 'px';\n$this-\u003estyleAttributes['height'] = $height . 'px';\n```\n\nIt 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:\n\n```php\n$style .= $key . ': ' . $value . ';';\n```\n\nThe 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.\n\n## PoC Evidence\n\nOn 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.\n\n## Remediation\n\nSanitize 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.","status":"published","published":"2026-09-16T22:14:53Z","modified":"2026-09-16T22:14:54Z","cvss":[{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:41:17.136201Z"}],"nvd":[{"id":"CVE-2026-58657","description":"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 \u003cimg style=...\u003e 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.","status":"published","published":"2026-07-08T14:17:20.44Z","modified":"2026-10-08T16:17:23.383Z","cvss":[{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"},{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:28.040501Z"}],"osv":[{"id":"CVE-2026-58657","title":"Grav - Stored CSS Injection via Markdown Image resize() Action","description":"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 \u003cimg style=...\u003e 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.","status":"published","published":"2026-07-08T13:49:12.183Z","modified":"2026-10-28T02:30:29.355394274Z","cvss":[{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:12.496548Z"},{"id":"GHSA-4wj4-79rr-pvff","title":"Duplicate Advisory: Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav","description":"### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-ffmg-hfvg-jhg9. This link is maintained to preserve external references.\n\n### Original Description\nGrav 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 \u003cimg style=...\u003e 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.","status":"withdrawn","published":"2026-07-08T15:32:01Z","modified":"2026-09-16T22:30:08.124418156Z","cvss":[{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium"},{"version":"4.0","score":4.8,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:37.528068Z"},{"id":"GHSA-ffmg-hfvg-jhg9","title":"Grav: Stored CSS injection via Markdown image resize() bypasses prior media style sanitizers in Grav","description":"## Summary\n\nGrav 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`.\n\nA publisher who can edit page Markdown can store a crafted image URL that renders additional CSS declarations in the final `\u003cimg style=...\u003e` attribute. This crosses the same lower-privileged publisher to higher-privileged reviewer/admin rendered-content boundary as the earlier media style and attribute advisories.\n\n## Impact\n\nA 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.\n\nThis 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.\n\n## Reproduction\n\nTested versions:\n\n- Grav 2.0 branch commit `6582166173bb8eb5869d96aea384e0e73777c94c`\n- Grav `2.0.0-rc.9` commit `e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8`\n\nMinimal Markdown payload:\n\n```markdown\n![logo](image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200)\n```\n\nA minimal PHPUnit-style reproducer can drive the same parser path directly:\n\n```php\n$m = new class {\n    use \\Grav\\Common\\Media\\Traits\\MediaObjectTrait;\n    use \\Grav\\Common\\Media\\Traits\\StaticResizeTrait;\n\n    public function addMetaFile($filepath) {}\n    public function __toString(): string { return ''; }\n    public function url($reset = true) { return '/img.png'; }\n    public function get($name, mixed $default = null, $separator = null) { return $default; }\n    public function set($name, mixed $value, $separator = null) { return $this; }\n    protected function createThumbnail($thumb) { return null; }\n    protected function createLink(array $attributes) { return null; }\n    protected function getItems(): array { return []; }\n};\n\n$excerpts = new \\Grav\\Common\\Page\\Markdown\\Excerpts(null, ['markdown' =\u003e [], 'images' =\u003e []]);\n$m = $excerpts-\u003eprocessMediaActions(\n    $m,\n    'image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200'\n);\n$element = $m-\u003eparsedownElement('', '', '', '', false);\nvar_dump($element['attributes']['style']);\n```\n\nObserved style attribute:\n\n```text\nwidth: 100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999px;height: 200px;\n```\n\nThe appended `px` lands on the final `z-index` value, but the preceding injected declarations remain syntactically valid CSS.\n\n## Root Cause / Technical Details\n\n`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)`.\n\nFor `resize()`, `system/src/Grav/Common/Media/Traits/StaticResizeTrait.php::resize()` stores width and height directly into style attributes:\n\n```php\n$this-\u003estyleAttributes['width'] = $width . 'px';\n$this-\u003estyleAttributes['height'] = $height . 'px';\n```\n\nIt 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:\n\n```php\n$style .= $key . ': ' . $value . ';';\n```\n\nThe 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.\n\n## PoC Evidence\n\nOn 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.\n\n## Remediation\n\nSanitize 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.","status":"published","published":"2026-09-16T22:14:53Z","modified":"2026-09-16T22:30:08.134393419Z","cvss":[{"version":"3.1","score":4.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:46.39441Z"}]}}
