Grav CMS vulnerable to stored XSS via Markdown media attribute() action
An authenticated user with page editing permissions can inject an executable JavaScript event-handler attribute into rendered image HTML through Grav's Markdown media action syntax.
The issue is caused by Markdown image query parameters being converted into callable media actions. The public attribute() media method can be reached this way, allowing an editor to set an arbitrary HTML attribute name and value on the generated image element.
For example, this Markdown:
```markdown
)
```
is rendered as an image tag containing an executable onload handler:
```html
<img onload="alert(document.domain)" alt="Quarterly market overview" src="/user/pages/03.campaigns/market-overview.gif?...">
```
This results in stored XSS when another user views the affected page. In a multi-user Grav installation, a lower-privileged page editor could use this to target administrators or reviewers who preview or view editor-controlled content.
Tested versions:
Suggested classification:
CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:H/VI:L/VA:N/SC:H/SI:L/SA:NCVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:NThe issue appears to come from this source-to-sink flow:
1. ParsedownGravTrait::inlineImage() processes Markdown images.
2. Excerpts::processImageExcerpt() resolves the referenced media object.
3. Excerpts::processMediaActions() parses the image URL query string into media actions.
4. call_user_func_array() invokes the requested action method on the media object.
5. MediaObjectTrait::attribute() stores the attacker-controlled attribute name and value.
6. The media object returns a Parsedown element containing the injected attribute.
7. Parsedown renders the attribute name into the final HTML.
Relevant code paths:
```text
system/src/Grav/Common/Markdown/ParsedownGravTrait.php
system/src/Grav/Common/Page/Markdown/Excerpts.php
system/src/Grav/Common/Media/Traits/MediaObjectTrait.php
system/src/Grav/Common/Page/Medium/StaticImageMedium.php
system/src/Grav/Common/Page/Medium/ImageMedium.php
vendor/erusev/parsedown/Parsedown.php
```
In system/src/Grav/Common/Markdown/ParsedownGravTrait.php, Markdown image excerpts are passed into Grav-specific media handling:
```php
if (isset($excerpt['element']['attributes']['src'])) {
$excerpt = $this->excerpts->processImageExcerpt($excerpt);
}
```
In system/src/Grav/Common/Page/Markdown/Excerpts.php, query string parameters are converted into media action calls. The query parameter name becomes the method name:
```php
$carry[] = ['method' => $parts[0], 'params' => $value];
```
The requested method is later invoked dynamically:
```php
$medium = call_user_func_array([$medium, $action['method']], $args);
```
For the payload:
```text
attribute=onload,alert(document.domain)
```
the method is attribute, and the arguments are onload and alert(document.domain).
In system/src/Grav/Common/Media/Traits/MediaObjectTrait.php, attribute() stores the caller-controlled attribute name directly:
```php
public function attribute($attribute = null, $value = '')
{
if (!empty($attribute)) {
$this->attributes[$attribute] = $value;
}
return $this;
}
```
The image media classes then return the collected attributes as attributes for an img element.
In system/src/Grav/Common/Page/Medium/StaticImageMedium.php:
```php
return ['name' => 'img', 'attributes' => $attributes];
```
The non-static image path in system/src/Grav/Common/Page/Medium/ImageMedium.php also returns image attributes in the same way.
Finally, in vendor/erusev/parsedown/Parsedown.php, the attribute value is escaped, but the attribute name is rendered as-is:
```php
$markup .= ' '.$name.'="'.self::escape($value).'"';
```
As a result, the attacker-controlled attribute name onload is emitted into the final HTML and executes as a browser event handler.
The Admin Plugin's save-time XSS detection does not appear to block this because the stored content is Markdown media syntax, not raw HTML:
```markdown
)
```
The dangerous HTML is generated later during Markdown/media rendering.
I reproduced this on a standard Grav CMS installation with the Admin Plugin enabled.
Configuration and prerequisites:
This issue can be reached over the network, attack complexity is low, an attacker needs administrative privileges on the target. A user must be tricked into taking some action. The scope is changed, meaning a successful attack can affect components beyond the vulnerable one. Rated impact: confidentiality low, integrity low, availability none.
The score comes from this vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N
CVE-2026-42841 is classified as CWE-79: Cross-site Scripting (XSS). User-supplied data is written into a page without escaping, so attacker script runs in the browser of anyone who views it.
CVE-2026-42841 is recorded against 2 packages.
Published on 5 May 2026 and last revised on 13 May 2026. No public exploit is currently recorded for this entry. A vendor advisory or fix has been published. Record sourced from NVD.
github.com (Web)
nvd.nist.gov (Advisory)
github.com (Web)
github.com (Package)
getgrav/grav has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-79: Cross-site Scripting (XSS)) in other software:
Details
CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| getgrav/grav | — | 2.0.0-beta.2 |
| grav | — | — |
References
Similar Threats
Vulnerability Monitoring
CVE-2026-42841 is rated CVSS 4.8 Medium. BotEraser monitors your WordPress installation and notifies you when software you use appears in our vulnerability database.
Set Up Free Alerts →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.