{"id":"GHSA-mxm6-v9r6-r94c","summary":"@nuxtjs/mdc's URL sanitizer misses SVG xlink:href and data:text/html, allowing XSS from untrusted markdown at the default configuration","details":"## Summary\n\n`@nuxtjs/mdc` renders untrusted markdown (including raw HTML) to a Vue component tree. Across two prior advisories it added a URL/attribute sanitizer to block dangerous links in that HTML: `validateProps` / `validateProp` and an `unsafeLinkPrefix` deny-list (`dist/runtime/parser/utils/props.js`). The sanitizer runs at parse time (`dist/runtime/parser/compiler.js`) and `parseMarkdown` enables raw HTML by default (`allowDangerousHtml: true`, `dist/runtime/parser/options.js`), so the sanitizer is the only barrier and it applies with no configuration required.\n\nTwo sibling vectors bypass that sanitizer at the default configuration:\n\n1. SVG anchor `xlink:href`. `validateProp` only scheme-checks attributes named exactly `href` or `src`:\n\n   ```\n   if (attribute === \"href\" || attribute === \"src\") return isAnchorLinkAllowed(value);\n   return true;\n   ```\n\n   An `xlink:href` (parsed to the hast property `xLinkHref`) is neither, so a `javascript:` URL on an SVG `\u003ca\u003e` is passed through. The renderer maps the property back to the real attribute (`MDCRenderer.vue`: `find(html, \"xLinkHref\").attribute` is `xlink:href`), so the output element is `\u003ca xlink:href=\"javascript:...\"\u003e`. Clicking it runs the script in the page origin. Plain `\u003ca href=\"javascript:...\"\u003e` is correctly stripped, which is what makes this the un-patched sibling.\n\n2. `\u003ciframe src=\"data:text/html,...\"\u003e`. `data:text/html` is present in `unsafeLinkPrefix`, but the check compares it against `url.protocol`:\n\n   ```\n   if (unsafeLinkPrefix.some((prefix) =\u003e url.protocol.toLowerCase().startsWith(prefix))) return false;\n   ```\n\n   For any data URI `url.protocol` is just `\"data:\"`, so `\"data:\".startsWith(\"data:text/html\")` is always false. Every `data:text/*` entry in the deny-list is therefore dead code, and `\u003ciframe src=\"data:text/html,\u003cscript\u003e...\u003c/script\u003e\"\u003e` is allowed (iframe is not in the render-time `dangerousTags`, which is only `[\"script\",\"base\"]`). The framed document executes script in an opaque origin. For contrast, `srcdoc` and `object` are blocked, so this is a precise gap rather than a general absence of filtering.\n\n## Reproduction\n\nI will attach the zip file for POC, you can simply extract and run `./poc.sh` to install mdc and show the poc in the html file.\n[nuxtjs-mdc-xss_poc.zip](https://github.com/user-attachments/files/29175603/nuxtjs-mdc-xss_poc.zip)\n\nTwo zero-argument checks:\n\n1. `sh poc/poc.sh` installs `@nuxtjs/mdc` and runs `parseMarkdown` (the documented API) at default. It shows the parsed tree retains `a { xLinkHref: \"javascript:...\" }` and `iframe { src: \"data:text/html,...\" }`, while the control payloads `href=\"javascript:...\"` and `srcdoc=...` are removed by the sanitizer. This isolates the sanitizer bypass deterministically.\n2. `poc/poc.sh` also serves `poc/poc.html` over http (data: iframes and javascript: links are restricted under the file:// origin, so http is used). Open the printed URL and click the blue SVG link. The page contains the exact DOM the renderer produces for those parsed nodes; clicking the SVG link executes script in the page origin (same-origin), and the data:text/html iframe executes on load. The page prints VULNERABLE for each that fires.\n\nBoth vectors were confirmed executing in a current Chromium build: the SVG `xlink:href` link runs script in the document origin on click, and the data:text/html iframe runs script on load.\n\n## Suggested fix\n\nIn `validateProp`, scheme-check `xlink:href` (and the hast `xLinkHref`) the same way as `href`/`src`. In `isAnchorLinkAllowed`, compare the dangerous MIME-typed entries against the full URL (or `href`), not against `url.protocol`, so `data:text/html` is actually matched; or add `iframe` to the render-time dangerous-tag set / restrict iframe `src` schemes.","aliases":["CVE-2026-63671"],"modified":"2026-09-16T22:30:08.107441971Z","published":"2026-09-16T22:14:01Z","database_specific":{"github_reviewed_at":"2026-09-16T22:14:01Z","nvd_published_at":"2026-09-16T15:17:40Z","cwe_ids":["CWE-184","CWE-79"],"severity":"HIGH","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/nuxt-content/mdc/security/advisories/GHSA-mxm6-v9r6-r94c"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63671"},{"type":"WEB","url":"https://github.com/nuxt-content/mdc/pull/491"},{"type":"WEB","url":"https://github.com/nuxt-content/mdc/commit/61d636c2983f021288e4fc5c4006733b38cf0d53"},{"type":"PACKAGE","url":"https://github.com/nuxt-content/mdc"},{"type":"WEB","url":"https://github.com/nuxt-content/mdc/releases/tag/v0.22.1"}],"affected":[{"package":{"name":"@nuxtjs/mdc","ecosystem":"npm","purl":"pkg:npm/%40nuxtjs/mdc"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.22.1"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-mxm6-v9r6-r94c/GHSA-mxm6-v9r6-r94c.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"}]}