GHSA-M3Q2-P4FW-W38M
Vulnerability from github – Published: 2026-06-16 23:38 – Updated: 2026-06-16 23:38Impact
Nuxt's globally registered <NoScript> component (from @unhead/vue head components, re-exported by Nuxt) wrote its default-slot content to the innerHTML of the <noscript> head tag, bypassing the HTML escaping that {{ }} interpolation normally applies in Vue templates.
Applications that placed untrusted, attacker-controllable data inside a <NoScript> slot, for example:
<NoScript>{{ route.query.banner }}</NoScript>
would emit that value unescaped inside <noscript> in the server-rendered HTML. With scripting enabled, the HTML parser treats <noscript> content in <head> under the "in head noscript" insertion mode: any tag other than link, meta, noframes, or style implicitly closes <noscript> and is re-processed in the head. A payload such as <script>...</script> therefore escapes the element and executes in the document context.
Sibling head components (<Style>, <Title>) were not affected because they already routed slot text through the safe textContent path.
Affected versions
All currently supported versions of nuxt that ship the <NoScript> global component.
Patches
Fixed in nuxt@4.4.7 (commit 4b054e9d) and backported to nuxt@3.21.7 (commit 7fea9fd6). The fix escapes <NoScript> slot content with escapeHtml from @vue/shared and writes it to textContent rather than innerHTML. Slot content is now rendered as text; intentional markup inside <NoScript> is no longer parsed as HTML.
Workarounds
Until you can upgrade:
- Do not interpolate untrusted input into
<NoScript>slots. Replace<NoScript>{{ x }}</NoScript>with a static string, or sanitise / HTML-escapexat the source. - If you must render dynamic noscript content, write the tag yourself via
useHead({ noscript: [{ textContent: escapedValue }] })after escapingescapedValue.
Credit
Reported to Anthropic's coordinated vulnerability disclosure pipeline by Claude (Anthropic's AI assistant) and triaged by the Anthropic security team. Reference: ANT-2026-4NJYDFFM.
Independently reported by @alcls01111 via GitHub's coordinated disclosure flow (GHSA-8grp-wcq9-925q), closed as a duplicate of this advisory.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "nuxt"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.4.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "nuxt"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.21.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-16T23:38:47Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "### Impact\n\nNuxt\u0027s globally registered `\u003cNoScript\u003e` component (from `@unhead/vue` head components, re-exported by Nuxt) wrote its default-slot content to the `innerHTML` of the `\u003cnoscript\u003e` head tag, bypassing the HTML escaping that `{{ }}` interpolation normally applies in Vue templates.\n\nApplications that placed untrusted, attacker-controllable data inside a `\u003cNoScript\u003e` slot, for example:\n\n```vue\n\u003cNoScript\u003e{{ route.query.banner }}\u003c/NoScript\u003e\n```\n\nwould emit that value unescaped inside `\u003cnoscript\u003e` in the server-rendered HTML. With scripting enabled, the HTML parser treats `\u003cnoscript\u003e` content in `\u003chead\u003e` under the \"in head noscript\" insertion mode: any tag other than `link`, `meta`, `noframes`, or `style` implicitly closes `\u003cnoscript\u003e` and is re-processed in the head. A payload such as `\u003cscript\u003e...\u003c/script\u003e` therefore escapes the element and executes in the document context.\n\nSibling head components (`\u003cStyle\u003e`, `\u003cTitle\u003e`) were not affected because they already routed slot text through the safe `textContent` path.\n\n### Affected versions\n\nAll currently supported versions of `nuxt` that ship the `\u003cNoScript\u003e` global component.\n\n### Patches\n\nFixed in `nuxt@4.4.7` (commit [`4b054e9d`](https://github.com/nuxt/nuxt/commit/4b054e9d95f8daf366cb144b52782047c511a66e)) and backported to `nuxt@3.21.7` (commit [`7fea9fd6`](https://github.com/nuxt/nuxt/commit/7fea9fd687f1dacbfb63db5fae5839896b017a0e)). The fix escapes `\u003cNoScript\u003e` slot content with `escapeHtml` from `@vue/shared` and writes it to `textContent` rather than `innerHTML`. Slot content is now rendered as text; intentional markup inside `\u003cNoScript\u003e` is no longer parsed as HTML.\n\n### Workarounds\n\nUntil you can upgrade:\n\n- Do not interpolate untrusted input into `\u003cNoScript\u003e` slots. Replace `\u003cNoScript\u003e{{ x }}\u003c/NoScript\u003e` with a static string, or sanitise / HTML-escape `x` at the source.\n- If you must render dynamic noscript content, write the tag yourself via `useHead({ noscript: [{ textContent: escapedValue }] })` after escaping `escapedValue`.\n\n### Credit\n\nReported to Anthropic\u0027s coordinated vulnerability disclosure pipeline by Claude (Anthropic\u0027s AI assistant) and triaged by the Anthropic security team. Reference: ANT-2026-4NJYDFFM.\n\nIndependently reported by [@alcls01111](https://github.com/alcls01111) via GitHub\u0027s coordinated disclosure flow (`GHSA-8grp-wcq9-925q`), closed as a duplicate of this advisory.",
"id": "GHSA-m3q2-p4fw-w38m",
"modified": "2026-06-16T23:38:47Z",
"published": "2026-06-16T23:38:47Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nuxt/nuxt/security/advisories/GHSA-m3q2-p4fw-w38m"
},
{
"type": "WEB",
"url": "https://github.com/nuxt/nuxt/commit/4b054e9d95f8daf366cb144b52782047c511a66e"
},
{
"type": "WEB",
"url": "https://github.com/nuxt/nuxt/commit/7fea9fd687f1dacbfb63db5fae5839896b017a0e"
},
{
"type": "PACKAGE",
"url": "https://github.com/nuxt/nuxt"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Cross-site scripting via \u003cNoScript\u003e slot content in Nuxt\u0027s head components"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.