GitHub Security Advisory: GHSA-r7fx-8g49-7hhr
Package: getgrav/grav (COMPOSER)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: < 2.0.0-beta.2
Patched in: 2.0.0-beta.2

Summary: Grav CMS vulnerable to stored XSS via Markdown media attribute() action

Details:
### Summary

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
![Quarterly market overview](market-overview.gif?attribute=onload,alert(document.domain))
```

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:

- Grav CMS: 1.7.49.5
- Admin Plugin: 1.10.49.1

Suggested classification:

- CWE-79: Improper Neutralization of Input During Web Page Generation
- Stored Cross-Site Scripting
- Suggested CVSS v4.0 score if page editing is considered high privilege: 6.9 Medium
- Suggested CVSS v4.0 vector: `CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:H/VI:L/VA:N/SC:H/SI:L/SA:N`
- Suggested CVSS v3.1 score if page editing is considered high privilege: 6.9 Medium
- Suggested CVSS v3.1 vector: `CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N`

### Details

The 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[] =