CVE-2026-63671: @nuxtjs/mdc Cross-Site Scripting Vulnerability

HIGH
Advisory Identifier: GHSA-mxm6-v9r6-r94c
CVSS 8.1
CWE-79 CWE-184
Published
Advisory Summary

CVE-2026-63671 is a HIGH (CVSS 8.1) cross-site scripting vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting @nuxtjs/mdc libraries in the npm ecosystem. The issue involves href and data:text/html, allowing xss from untrusted markdown at the default configuration. Remediation is available by upgrading affected packages to version 0.22.1.

CVE-2026-63671 at a Glance

CVE Identifier CVE-2026-63671
GHSA Identifier GHSA-mxm6-v9r6-r94c
Severity HIGH
CVSS v3.1 Base Score 8.1
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product @nuxtjs/mdc
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-63671

To remediate this vulnerability, upgrade all affected @nuxtjs/mdc dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@nuxtjs/mdc fixed: 0.22.1
npm install @nuxtjs/mdc@0.22.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @nuxtjs/mdc

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-63671, inspect your project dependencies using npm:

npm ls @nuxtjs/mdc

Analyze the terminal output to locate any version of @nuxtjs/mdc in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.22.1), your application is affected.

Note: The npm ls @nuxtjs/mdc command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In @nuxtjs/mdc, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary `@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: 1. 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. 2. `<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. ## Reproduction 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](https://github.com/user-attachments/files/29175603/nuxtjs-mdc-xss_poc.zip) Two zero-argument checks: 1. `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. 2. `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. ## Suggested fix 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.

CVE-2026-63671 Affected @nuxtjs/mdc Versions

1 component(s)
Fixed Versions:
0.22.1
Product / Package Ecosystem Affected Versions Fixed Version
@nuxtjs/mdc
npm
< 0.22.1
0.22.1

References

Frequently Asked Questions

What is CVE-2026-63671?
CVE-2026-63671 is a security vulnerability in @nuxtjs/mdc involving href and data:text/html, allowing XSS from untrusted markdown at the default configuration under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which @nuxtjs/mdc versions are affected?
Affected versions include releases prior to 0.22.1 across affected packages: @nuxtjs/mdc.
What version fixes CVE-2026-63671?
Upgrading to 0.22.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @nuxtjs/mdc affected?
Yes, @nuxtjs/mdc is affected in releases prior to 0.22.1. Upgrade your dependency to 0.22.1 or later.
What is the CVSS score of CVE-2026-63671?
CVE-2026-63671 has a CVSS v3.1 base score of 8.1 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-63671?
CVE-2026-63671 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check npm dependencies for CVE-2026-63671?
Run the dependency tree command: 'npm ls @nuxtjs/mdc'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-mxm6-v9r6-r94c",
  "modified" : "2026-09-16T22:14:01Z",
  "published" : "2026-09-16T22:14:01Z",
  "aliases" : [ "CVE-2026-63671" ],
  "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 `<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.\n\n2. `<iframe src=\"data:text/html,...\">`. `data:text/html` is present in `unsafeLinkPrefix`, but the check compares it against `url.protocol`:\n\n   ```\n   if (unsafeLinkPrefix.some((prefix) => 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 `<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.\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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@nuxtjs/mdc"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.22.1"
      } ]
    } ]
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-184", "CWE-79" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-16T22:14:01Z",
    "nvd_published_at" : "2026-09-16T15:17:40Z"
  }
}