@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.
Two sibling vectors bypass that sanitizer at the default configuration:
SVG anchor xlink:href. validateProp only scheme-checks attributes named exactly href or src:
if (attribute === "href" || attribute === "src") return isAnchorLinkAllowed(value);
return true;
An xlink:href (parsed to the hast property xLinkHref) is neither, so a javascript: URL on an SVG <a> 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 <a xlink:href="javascript:...">. Clicking it runs the script in the page origin. Plain <a href="javascript:..."> is correctly stripped, which is what makes this the un-patched sibling.
<iframe src="data:text/html,...">. data:text/html is present in unsafeLinkPrefix, but the check compares it against url.protocol:
if (unsafeLinkPrefix.some((prefix) => url.protocol.toLowerCase().startsWith(prefix))) return false;
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 <iframe src="data:text/html,<script>...</script>"> 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.
I 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.
nuxtjs-mdc-xss_poc.zip
Two zero-argument checks:
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.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.Both 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.
In 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.
{
"cwe_ids": [
"CWE-184",
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-16T22:14:01Z",
"nvd_published_at": "2026-09-16T15:17:40Z",
"severity": "HIGH"
}