Skip to content

chore(deps): update devdependency unhead to v2.1.11 [security]#925

Merged
danielroe merged 1 commit intomainfrom
renovate/npm-unhead-vulnerability
Mar 12, 2026
Merged

chore(deps): update devdependency unhead to v2.1.11 [security]#925
danielroe merged 1 commit intomainfrom
renovate/npm-unhead-vulnerability

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Mar 12, 2026

This PR contains the following updates:

Package Change Age Confidence
unhead (source) 2.1.102.1.11 age confidence

GitHub Vulnerability Alerts

CVE-2026-31873

The link.href check in makeTagSafe (safe.ts, line 68-71) uses String.includes(), which is case-sensitive:

if (key === 'href') {
  if (val.includes('javascript:') || val.includes('data:')) {
    return
  }
  next[key] = val
}

Browsers treat URI schemes case-insensitively. DATA:text/css,... is the same as data:text/css,... to the browser, but 'DATA:...'.includes('data:') returns false.

PoC

useHeadSafe({
  link: [{
    rel: 'stylesheet',
    href: 'DATA:text/css,body{display:none}'
  }]
})

SSR output:

<link rel="stylesheet" href="DATA:text/css,body{display:none}">

The browser loads this as a CSS stylesheet. An attacker can inject arbitrary CSS for UI redressing or data exfiltration via CSS attribute selectors with background-image callbacks.

Any case variation works: DATA:, Data:, dAtA:, JAVASCRIPT:, etc.

Suggested fix

if (key === 'href') {
  const lower = val.toLowerCase()
  if (lower.includes('javascript:') || lower.includes('data:')) {
    return
  }
  next[key] = val
}

CVE-2026-31860

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.

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:

tag.props = { ...acceptDataAttrs(prev), ...next }

Then propsToString (propsToString.ts, line 26) interpolates property keys directly into the HTML string with no sanitization:

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:

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

SSR output:

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

The browser parses onload=alert(document.domain) as its own attribute. Once the stylesheet loads, the handler fires.

The same injection works on any tag type since acceptDataAttrs is applied to all of them at line 114. Here's the same thing on a meta tag (the injected attributes render, though onclick doesn't fire on non-interactive <meta> elements):

useHeadSafe({
  meta: [{
    name: 'description',
    content: 'legitimate content',
    'data-x onclick=alert(document.domain) y': 'z'
  }]
})

Realistic scenario

A Nuxt app accepts SEO metadata from a CMS or user profile. The developer uses useHeadSafe() as the docs recommend. An attacker puts a data-* key with spaces and an event handler into their input. The payload renders into the HTML on every page load.

Suggested fix

For vulnerability 1, validate that attribute names only contain characters legal in HTML attributes:

const SAFE_ATTR_RE = /^[a-zA-Z][a-zA-Z0-9\-]*$/

function acceptDataAttrs(value: Record<string, string>) {
  return Object.fromEntries(
    Object.entries(value || {}).filter(
      ([key]) => (key === 'id' || key.startsWith('data-')) && SAFE_ATTR_RE.test(key)
    ),
  )
}

Release Notes

unjs/unhead (unhead)

v2.1.11

Compare Source

    ⚠️ Security
  • Fixed XSS bypass in useHeadSafe via attribute name injection (GHSA-g5xx-pwrp-g3fv). Users handling untrusted input with useHeadSafe should upgrade immediately.
   🐞 Bug Fixes
    View changes on GitHub

Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@netlify
Copy link

netlify bot commented Mar 12, 2026

Deploy Preview for friendly-lamington-fb5690 ready!

Name Link
🔨 Latest commit 9522730
🔍 Latest deploy log https://app.netlify.com/projects/friendly-lamington-fb5690/deploys/69b2d7765bb5a70007ffde36
😎 Deploy Preview https://deploy-preview-925--friendly-lamington-fb5690.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov
Copy link

codecov bot commented Mar 12, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 30.18%. Comparing base (2f736b9) to head (9522730).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #925   +/-   ##
=======================================
  Coverage   30.18%   30.18%           
=======================================
  Files          12       12           
  Lines         328      328           
  Branches       98       98           
=======================================
  Hits           99       99           
  Misses        229      229           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@danielroe danielroe merged commit 393d6fe into main Mar 12, 2026
10 checks passed
@danielroe danielroe deleted the renovate/npm-unhead-vulnerability branch March 12, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant