Unhead has XSS bypass in `useHeadSafe` via attribute name injection and case-sensitive protocol check
Date: 2026-03
Affected: npm/unhead
Attack vector: xss
Category: vulnerability-disclosure
Severity: Medium
CVE: CVE-2026-31860
CWE: CWE-79

## Summary `useHeadSafe()` can be bypassed to inject arbitrary HTML attributes, including event handlers, into SSR-rendered `<head>` tags. This is the composable that Nuxt docs recommend for safely handling user-generated content. ## Details **XSS via `data-*` attribute name injection** The `acceptDataAttrs` function (safe.ts, line 16-20) allows any property key starting with `data-` through to the final HTML. It only checks the prefix, not whether the key contains spaces or other characters that break HTML attribute parsing. ```typescript function acceptDataAttrs(value: Record<string, string>) { return Object.fromEntries( Object.entries(value || {}).filter(([key]) => key === 'id' || key.startsWith('data-')), ) } ``` This result gets merged into every tag's props at line 114: ```typescript tag.props = { ...acceptDataAttrs(prev), ...next } ``` Then `propsToString` (propsToString.ts, line 26) interpolates property keys directly into the HTML string with no sanitization: ```typescript attrs += value === true ? ` ${key}` : ` ${key}="${encodeAttribute(value)}"` ``` A space in the key breaks out of the attribute name. Everything after the space becomes separate HTML attributes. ### PoC The most practical vector uses a `link` tag. `<link rel="stylesheet">` fires `onload` once the stylesheet loads, giving reliable script execution: ```javascript useHeadSafe({ link: [{ rel: 'stylesheet', href: '/valid-stylesheet.css', 'data-x onload=alert(document.do


Mitigations:

Architectural layers (MAESTRO):