Skip to main content
CVE Vulnerability Database
Vulnerability Database/CVE-2026-39315

CVE-2026-39315: Unhead useHeadSafe() XSS Vulnerability

CVE-2026-39315 is an XSS vulnerability in Unhead's useHeadSafe() composable that allows attackers to bypass safety checks using padded HTML entities. This article covers the technical details, affected versions, and patches.

Published:

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:

  1. Identifying an application using Unhead's useHeadSafe() to render user-controlled content
  2. Crafting a payload with excessively padded numeric character references (e.g., javascript:alert(1) to encode javascript:alert(1))
  3. Submitting this payload through the vulnerable input vector
  4. 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:

typescript
 
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
bash
# 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.

Default Legacy - Prefooter | Experience the World’s Most Advanced Cybersecurity Platform

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.