---
id: CVE-2026-58657
aliases:
  - GHSA-ffmg-hfvg-jhg9
title: >-
  Grav: Stored CSS injection via Markdown image resize() bypasses prior media
  style sanitizers in Grav
summary: >-
  Grav: Stored CSS injection via Markdown image resize() bypasses prior media
  style sanitizers in Grav
severity: medium
cvss: 4.8
cwe:
  - CWE-79
vendor: getgrav
product: getgrav/grav
ecosystem: composer
affected:
  - getgrav/grav = 2.0.0-rc.9
patched:
  - getgrav/grav 2.0.0
published: '2026-09-16'
updated: '2026-09-16'
sourceUpdated: '2026-09-16T22:14:54Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-ffmg-hfvg-jhg9'
references:
  - url: 'https://github.com/getgrav/grav/security/advisories/GHSA-ffmg-hfvg-jhg9'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-58657'
  - url: >-
      https://github.com/getgrav/grav/commit/6582166173bb8eb5869d96aea384e0e73777c94c
  - url: >-
      https://github.com/getgrav/grav/commit/e03d29aa0d3ece16d73c1ffccfa78df8bf5f28b8
  - url: >-
      https://www.vulncheck.com/advisories/grav-stored-css-injection-via-markdown-image-resize-action
  - url: 'https://github.com/advisories/GHSA-ffmg-hfvg-jhg9'
tags:
  - ghsa
  - composer
epss: 0.00371
epssPercentile: 0.31054
ingestedAt: '2026-09-16T23:07:58.093Z'
---

## Overview

## 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](image.png?resize=100;position:fixed;top:0;left:0;width:100vw;height:100vh;background:white;z-index:9999,200)
```

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 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.

## Affected packages

- `getgrav/grav = 2.0.0-rc.9`

## Remediation

Upgrade to a patched release:

- `getgrav/grav 2.0.0`
