Nuxt/Unhead XSS Bypass, CVE-2026-31860 (Critical)

Listen to this Post

The vulnerability resides in the `acceptDataAttrs` function within Unhead’s `safe.ts` (lines 16-20). This function is designed to filter attributes for the `useHeadSafe()` composable, which is specifically recommended by Nuxt documentation for handling user-generated content safely . The code intends to allow only `id` attributes and those with a `data-` prefix, passing them through to the final HTML. However, it only performs a prefix check and fails to validate the overall syntax of the attribute key. Consequently, an attacker can inject a key like data-x onload=alert(document.domain) y. Because the filter does not reject keys containing spaces, the `propsToString` function (line 26) later interpolates this malicious string directly into the SSR output. The space breaks the attribute declaration, causing the browser to parse `onload=alert(document.domain)` as a separate, legitimate event handler attribute. When the tag (e.g., a <link rel="stylesheet">) loads, the injected JavaScript executes, leading to Cross-site Scripting (XSS) .

dailycve form:

Platform: Nuxt/Unhead
Version: before 2.1.11
Vulnerability: XSS Bypass
Severity: Critical
date: 12 March 2026

Prediction: Patched in 2.1.11

What Undercode Say:

Analytics

  • Attack Vector: Network
  • Complexity: Low
  • Privileges: None
  • User Interaction: Required
  • Scope: Changed
  • CVSS Score: 5.3 (Medium)

Bash/Code

Identify vulnerable versions (package.json)
grep '"unhead":' package.json
Vulnerable range: < 2.1.11
Upgrade command to patch
npm install @unhead/[email protected]
or
yarn add @unhead/[email protected]

Vulnerable Code Snippet (safe.ts)

// Lines 16-20: Vulnerable filter
function acceptDataAttrs(value: Record<string, string>) {
return Object.fromEntries(
Object.entries(value || {}).filter(([bash]) => key === 'id' || key.startsWith('data-')),
)
}
// No regex validation on the 'key' allows spaces and special chars.

How Exploit

1. Vector: SSR-rendered `` tags (e.g., `link`, `meta`).

2. Method: Inject a malformed attribute key.

3. Payload Example:

useHeadSafe({
link: [{
rel: 'stylesheet',
href: '/valid.css',
'data-x onload=alert(document.domain) y': 'z'
}]
})

4. Resulting HTML:

<link data-x onload=alert(document.domain) y="z" rel="stylesheet" href="/valid.css">

The browser parses `onload=alert(document.domain)` as a valid event handler.

Protection from this CVE

  1. Patch: Upgrade to `@unhead/[email protected]` or later .
  2. Code Fix: Implement strict regex validation for attribute names.
    const SAFE_ATTR_RE = /^[a-zA-Z][a-zA-Z0-9-]$/;
    function acceptDataAttrs(value: Record<string, string>) {
    return Object.fromEntries(
    Object.entries(value || {}).filter(
    ([bash]) => (key === 'id' || key.startsWith('data-')) && SAFE_ATTR_RE.test(key)
    ),
    );
    }
    
  3. Workaround: Avoid using `useHeadSafe()` with raw user input containing attributes; sanitize keys manually.

Impact

  • Confidentiality: Low (DOM access)
  • Integrity: Low (script execution)
  • Availability: None
  • Real-world Scenario: A Nuxt application accepting SEO metadata from a CMS or user profile. An attacker injects a `data-` key with spaces and an event handler into their input. The payload renders into the HTML on every page load, allowing session hijacking, defacement, or redirection to malicious sites.

🎯Let’s Practice Exploiting & Learn Patching For Free:

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top