CVE-2026-39315 Overview
CVE-2026-39315 is a Cross-Site Scripting (XSS) vulnerability in Unhead, a document head and template manager commonly used with Nuxt.js applications. The vulnerability exists in the useHeadSafe() composable, which Nuxt's documentation explicitly recommends for rendering user-supplied content in <head> safely. Prior to version 2.1.13, the hasDangerousProtocol() function in packages/unhead/src/plugins/safe.ts contains a flawed HTML entity decoder that can be bypassed using padded numeric character references, allowing attackers to inject malicious JavaScript URIs.
Critical Impact
Attackers can bypass XSS protections in Unhead's safe composable by using padded HTML entities, enabling JavaScript execution in server-side rendered HTML output that affects end users.
Affected Products
- Unhead versions prior to 2.1.13
- Nuxt.js applications using useHeadSafe() with user-supplied content
- Any application relying on Unhead's safe mode for sanitizing head elements
Discovery Timeline
- 2026-04-09 - CVE CVE-2026-39315 published to NVD
- 2026-04-09 - Last updated in NVD database
Technical Details for CVE-2026-39315
Vulnerability Analysis
The vulnerability stems from an incomplete implementation of HTML entity decoding within Unhead's XSS protection mechanism. The hasDangerousProtocol() function is designed to block dangerous URI schemes such as javascript:, data:, and vbscript: by first decoding HTML entities and then checking if the decoded string starts with a blocked scheme.
The critical flaw lies in the regular expressions used for decoding numeric character references. The original implementation used fixed-width digit caps in the regex patterns: &#x([0-9a-f]{1,6});? for hexadecimal entities and &#(\\d{1,7});? for decimal entities. However, the HTML5 specification imposes no limit on leading zeros in numeric character references.
When an attacker crafts a padded entity that exceeds the regex digit cap (e.g., j for the letter 'j'), the decoder silently skips it because the regex doesn't match. The undecoded string is then passed to the startsWith('javascript:') check, which doesn't find a match since the string still contains the encoded entity. Subsequently, makeTagSafe() writes this raw value directly into SSR HTML output. When the browser's HTML parser processes the page, it natively decodes the padded entity and constructs the blocked URI, executing the malicious JavaScript.
Root Cause
The root cause is CWE-184: Incomplete List of Disallowed Inputs. The regex patterns for HTML entity decoding used arbitrary digit limits that don't align with the HTML5 specification's permissive handling of numeric character references. This creates a mismatch between server-side validation and client-side parsing behavior, allowing attackers to craft payloads that pass server validation but execute maliciously in the browser.
Attack Vector
The attack is network-based and requires user interaction, as a victim must visit a page containing the malicious payload. An attacker can exploit this vulnerability by:
- Identifying an application using Unhead's useHeadSafe() to render user-controlled content
- Crafting a payload with excessively padded numeric character references (e.g., javascript:alert(1) to encode javascript:alert(1))
- Submitting this payload through the vulnerable input vector
- When the SSR output is rendered in a victim's browser, the JavaScript executes in the context of the application
The following patch from GitHub commit 961ea78 shows the fix that removes the digit caps:
const SafeAttrName = /^[a-z][a-z0-9\-]*[a-z0-9]$/i
-const HtmlEntityHex = /&#x([0-9a-f]{1,6});?/gi
-const HtmlEntityDec = /&#(\d{1,7});?/g
+const HtmlEntityHex = /&#x([0-9a-f]+);?/gi
+const HtmlEntityDec = /&#(\d+);?/g
const HtmlEntityNamed = /&(tab|newline|colon|semi|lpar|rpar|sol|bsol|comma|period|excl|num|dollar|percnt|amp|apos|ast|plus|lt|gt|equals|quest|at|lsqb|rsqb|lcub|rcub|vert|hat|grave|tilde|nbsp);?/gi
// eslint-disable-next-line no-control-regex
const ControlChars = /[\\x00-\\x20]+/g
Source: GitHub Commit Details
Detection Methods for CVE-2026-39315
Indicators of Compromise
- Presence of excessively long numeric character references in HTTP request parameters or POST data (e.g., � patterns)
- Unusual HTML entities in <head> element attributes within application logs
- XSS attack patterns in user-submitted content targeting meta tags, link elements, or script sources
Detection Strategies
- Implement Web Application Firewall (WAF) rules to detect padded HTML numeric character references with more than 7 digits
- Review application logs for requests containing patterns like &#[0-9]{8,}; or &#x[0-9a-f]{7,};
- Deploy Content Security Policy (CSP) headers with strict script-src directives to mitigate successful exploitation
- Use static analysis tools to identify usage of useHeadSafe() with user-controlled input in Nuxt applications
Monitoring Recommendations
- Monitor for JavaScript errors or unexpected script execution originating from head elements
- Set up alerts for unusual patterns in request URLs and form submissions containing encoded URI schemes
- Enable CSP violation reporting to detect attempted XSS bypass attacks
How to Mitigate CVE-2026-39315
Immediate Actions Required
- Upgrade Unhead to version 2.1.13 or later immediately
- Audit all usages of useHeadSafe() to verify user input handling
- Implement Content Security Policy headers as a defense-in-depth measure
- Consider additional server-side validation of user-supplied content before passing to head composables
Patch Information
The vulnerability is fixed in Unhead version 2.1.13. The patch modifies the regex patterns in packages/unhead/src/plugins/safe.ts to accept numeric character references of arbitrary length, ensuring that padded entities are properly decoded before security checks are performed.
For detailed patch information, see the GitHub Security Advisory GHSA-95h2-gj7x-gx9w and the release notes for v2.1.13.
Workarounds
- If immediate upgrade is not possible, implement additional input sanitization before calling useHeadSafe()
- Use a strict allowlist approach for any user-supplied values in head elements
- Deploy Content Security Policy headers with script-src 'self' to block inline JavaScript execution
# Upgrade Unhead to the patched version
npm update unhead@2.1.13
# Or using yarn
yarn upgrade unhead@2.1.13
# Verify installed version
npm list unhead
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

