Media

Grav: Stored XSS via Markdown audio/video media <source> URL

  • Publicada: 17.09.2026
  • Actualizado: 11.10.2026 10:22

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](song.mp3#"><svg/onload=alert(1)>) 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 htmlspecialchars_decode(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](sound.mp3#"><svg/onload=alert(1)>)
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.

Software afectado

Haz clic en una tecnología para ver todas sus vulnerabilidades.

PlataformaPaqueteVersiones vulnerablesCorregida en
Paquete de Composer getgrav/grav < 2.0.15 2.0.15

Referencias

Puntuación CVSS 7.6/10
Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N
Debilidad (CWE)
CWE-79
Plataforma
Composer
Paquete
getgrav/grav
Publicada
17.09.2026
Actualizado
11.10.2026 10:22

Fuentes y licencias

OSV / GitHub Advisory Database - GHSA-6qw9-4vv5-jr97 - CC BY 4.0

Historial de vulnerabilidades: getgrav/grav

Todas las vulnerabilidades publicadas de este paquete (Paquete de Composer), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.

Todas (99) en el archivo
Entradas en total
99
Primera → última
11.2019 09.2026
Sin corrección
6
Explotada en ataques
0

Distribución por gravedad

  • Crítica 7
  • Alta 47
  • Media 41
  • Baja 4

Divulgaciones por año

  1. 2019: 1
  2. 2020: 1
  3. 2021: 4
  4. 2022: 10
  5. 2023: 5
  6. 2024: 8
  7. 2025: 23
  8. 2026: 47
Gravedad:
  1. Alta · 8.8 CVE-2026-72819 < 2.0.13 2.0.13
  2. Grav: Stored XSS via Markdown audio/video media <source> URL esta entrada
    Media · 7.6 CVE-2026-75831 < 2.0.15 2.0.15
  3. Alta CVE-2026-53653 >= 2.0.0-beta.1, < 2.0.0-rc.8, < 1.7.53 2.0.0-rc.8, 1.7.53
  4. Alta · 8.9 CVE-2026-42611 < 2.0.0-beta.2 2.0.0-beta.2
  5. Alta · 8.5 CVE-2026-42612 < 2.0.0-beta.2 2.0.0-beta.2
  6. Crítica · 9.4 CVE-2026-42613 < 2.0.0-beta.2 2.0.0-beta.2
  7. Crítica · 9.1 CVE-2025-66844 <= 1.7.49.5 sin corrección
  8. Alta CVE-2025-66298 < 1.8.0-beta.27 1.8.0-beta.27
  9. Alta CVE-2025-66294 < 1.8.0-beta.27 1.8.0-beta.27
  10. Alta · 8.5 CVE-2025-66300 < 1.8.0-beta.27 1.8.0-beta.27
  11. Alta · 8.8 CVE-2025-66299 < 1.8.0-beta.27 1.8.0-beta.27
  12. Alta · 8.5 CVE-2024-34082 < 1.7.46 1.7.46
  13. Alta · 8.8 CVE-2024-28119 < 1.7.45 1.7.45
  14. Alta · 8.8 CVE-2024-28118 < 1.7.45 1.7.45
  15. Alta · 8.8 CVE-2024-28117 < 1.7.45 1.7.45
  16. Alta · 8.8 CVE-2024-27921 < 1.7.45 1.7.45
  17. Alta · 8.8 CVE-2024-27923 < 1.7.43 1.7.43
  18. Alta · 7.2 CVE-2023-37897 < 1.7.42.2 1.7.42.2
  19. Alta · 7.2 CVE-2023-34448 < 1.7.42 1.7.42
  20. Alta · 7.2 CVE-2023-34252 < 1.7.42 1.7.42
  21. Crítica · 9.1 CVE-2023-34251 < 1.7.42 1.7.42
  22. Alta · 7.2 CVE-2022-2073 < 1.7.34 1.7.34
  23. Alta · 8.1 CVE-2020-29555 >= 1.7.0-beta.1, <= 1.7.0-rc.17, < 1.6.30 1.6.30
  24. Alta · 8.8 CVE-2020-29553 >= 1.7.0-beta.1, <= 1.7.0-rc.17, < 1.6.30 1.6.30
  25. Alta · 7.1 CVE-2022-0970 < 1.7.31 1.7.31
  26. Alta · 8.8 CVE-2021-3924 <= 1.7.24 sin corrección
  27. Alta · 8.4 CVE-2021-29440 < 1.7.11 1.7.11

Identifica las vulnerabilidades de tu aplicación

Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.