Cross-site scripting via <NoScript> slot content in Nuxt's head components
Impact
Nuxt's globally registered component (from `@unhead/vue` head components, re-exported by Nuxt) wrote its default-slot content to the `innerHTML` of the head tag, bypassing the HTML escaping that {{ }} interpolation normally applies in Vue templates.
Applications that placed untrusted, attacker-controllable data inside a `` slot, for example:
{{ route.query.banner }}
would emit that value unescaped inside in the server-rendered HTML. With scripting enabled, the HTML parser treats content in under the "in head noscript" insertion mode: any tag other than `link`, `meta`, `noframes`, or `style` implicitly closes and is re-processed in the head. A payload such as ... therefore escapes the element and executes in the document context.
Sibling head components (, ) 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 `` global component.
Patches
Fixed in [email protected] (commit 4b054e9d) and backported to [email protected] (commit 7fea9fd6). The fix escapes 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 is no longer parsed as HTML.
Workarounds
Until you can upgrade:
- Do not interpolate untrusted input into `` slots. Replace
{{ x }}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.