chore(deps): update devdependency unhead to v2.1.13 [security]#943
Open
renovate[bot] wants to merge 1 commit intomainfrom
Open
chore(deps): update devdependency unhead to v2.1.13 [security]#943renovate[bot] wants to merge 1 commit intomainfrom
renovate[bot] wants to merge 1 commit intomainfrom
Conversation
✅ Deploy Preview for friendly-lamington-fb5690 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #943 +/- ##
=======================================
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. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
2.1.12→2.1.13GitHub Vulnerability Alerts
CVE-2026-39315
##EVIDENCE
| Disclosed to Vercel H1 | 2026-03-22 (no response after 12 days) |
| Cross-reported here | 2026-04-03 |
Summary
useHeadSafe()is the composable that Nuxt's own documentation explicitly recommendsfor rendering user-supplied content in
<head>safely. Internally, thehasDangerousProtocol()function inpackages/unhead/src/plugins/safe.tsdecodesHTML entities before checking for blocked URI schemes (
javascript:,data:,vbscript:). The decoder uses two regular expressions with fixed-width digit caps:The HTML5 specification imposes no limit on leading zeros in numeric character
references. Both of the following are valid, spec-compliant encodings of
:(U+003A):&#​0000000058;— 10 decimal digits, exceeds the\d{1,7}cap:— 7 hex digits, exceeds the[0-9a-f]{1,6}capWhen a padded entity exceeds the regex digit cap, the decoder silently skips it. The
undecoded string is then passed to
startsWith('javascript:'), which does not match.makeTagSafe()writes the raw value directly into SSR HTML output. The browser's HTMLparser decodes the padded entity natively and constructs the blocked URI.
Root Cause Analysis
Vulnerable code (
packages/unhead/src/plugins/safe.ts, lines 10–11)Why the bypass works
The HTML5 parser specification ([§ Numeric character reference end state][html5-spec])
states that leading zeros in numeric character references are valid and the number of
digits is unbounded. A conformant browser will decode
:as:regardlessof the number of leading zeros.
Because the regex caps are lower than the digit counts an attacker can supply, the
entity match fails silently. The raw padded string (
java&#​0000000058;script:alert(1))is passed unchanged to the scheme check.
startsWith('javascript:')returnsfalse,and the value is rendered into SSR output verbatim. The browser then decodes the entity
and the blocked scheme is present in the live DOM.
Steps to Reproduce
Environment
Step 1 — Create a fresh Nuxt 4 project
npx nuxi init poc cd poc npm installStep 2 — Replace
pages/index.vueStep 3 — Start the dev server and inspect SSR output
In a separate terminal:
Expected result (safe)
Tags stripped entirely, or schemes rewritten to safe placeholder values.
Actual result (vulnerable)
Both
javascript:anddata:— explicitly enumerated in thehasDangerousProtocol()blocklist — are present in server-rendered HTML. The browser decodes the padded entities
natively on load.
Confirmed Execution Path (data: URI via iframe, Chrome 146+)
Immediate script execution from
<link>tags does not occur automatically — browsersdo not create a browsing context from
<link href>. The exploitability of this bypasstherefore depends on whether downstream application code consumes
<link>href values.This is a common pattern in real-world Nuxt applications:
<link>tags on the clientChrome 146+ permits
data:URIs loaded into iframes even though top-leveldata:navigation has been blocked since Chrome 60. The following snippet — representative
of any downstream consumer that forwards
<link href>into an iframe — triggersconfirmed script execution:
Full PoC with cookie exfiltration beacon
Observed result:
alert()fires from inside the iframe'sdata:document context:is present unescaped in the SSR-rendered<link>tagImpact
1. Broken security contract
Developers who follow Nuxt's own documentation and use
useHeadSafe()for untrusteduser input have no reliable protection against
javascript:,data:, orvbscript:scheme injection when that input contains leading-zero padded numeric character
references. The documented guarantee is silently violated.
2. Confirmed data: URI escape to SSR output
A fully valid
data:text/htmlURI now reaches server-rendered HTML. In applicationswhere any downstream code reads and loads
<link href>values (head managementutilities, SEO tooling, icon preview features), this is confirmed XSS — the payload
persists in SSR output and executes for every visitor whose browser triggers the
downstream consumption path.
3. Forward exploitability
If any navigation-context attribute (e.g.
<a href>,<form action>) is added to thesafe attribute whitelist in a future release, this bypass produces immediately
exploitable stored XSS with no additional attacker effort, because the end-to-end
bypass already works today.
Suggested Fix
Remove the fixed digit caps from both entity regexes. The downstream
safeFromCodePoint()function already validates that decoded codepoints fall within the valid Unicode range
(
> 0x10FFFF || < 0 || isNaN → ''), so unbounded digit matching introduces no newattack surface — it only ensures that all spec-compliant encodings of a codepoint are
decoded before the scheme check runs.
File:
packages/unhead/src/plugins/safe.ts, lines 10–11This is a minimal, low-risk change. No other code in the call path requires modification.
Weaknesses
References
allowed_uri?bypass (same root cause, accepted CVE)data:URI sanitizer bypass (same class)data:URIs blocked for top-level navigation since Chrome 60; permitted in iframesRelease Notes
unjs/unhead (unhead)
v2.1.13Compare Source
🐞 Bug Fixes
targetto array before merging potentialAction - by @harlan-zw and Claude Opus 4.6 (1M context) in #709 (22ac9)View changes on GitHub
Configuration
📅 Schedule: (UTC)
🚦 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.
This PR was generated by Mend Renovate. View the repository job log.