BTC
ETH
SOL
BNB
GOLD
XRP
DOGE
ADA
Back to home
Security

[MEDIUM] Security Advisory: Unhead has a hasDangerousProtocol() bypass via leading-zero padded HTML entities in useHeadSafe() (unhead)

Unhead's useHeadSafe() function, which Nuxt documentation recommends for injecting user-supplied content into <head> tags, contains a bypass in its hasDangerousProtocol() check.

Unhead’s useHeadSafe() function, which Nuxt documentation recommends for injecting user-supplied content into <head> tags, contains a bypass in its hasDangerousProtocol() check. Attackers pad HTML entities for dangerous characters like : with excessive leading zeros. This evades the protocol detection, allowing javascript:, data:, or vbscript: schemes to reach SSR output. Browsers then decode the entities natively, executing malicious code in attributes like href or src.

The vulnerability stems from regex patterns in unhead/src/plugins/safe.ts that cap digits in HTML entity decoders at six hexadecimal or seven decimal digits—matching Unicode code point limits but ignoring HTML5’s allowance for unlimited leading zeros. For example, : (decimal for :, U+003A) and : (hex) exceed these caps. The decoder skips them, passes the raw string to startsWith('javascript:') (which fails), and renders it verbatim. Chrome 146+ and other browsers parse it as intended, reconstructing the blocked URI.

Root Cause

Unhead decodes entities before scheme checks, but its patterns silently fail on over-padded refs:

// Vulnerable code in unhead 2.1.12, safe.ts lines 10-11
const HtmlEntityHex = /&#x([0-9a-f]{1,6});?/gi  // Caps at 6 hex digits
const HtmlEntityDec = /&#(\d{1,7});?/g          // Caps at 7 decimal digits

HTML5 spec (§12.2.4.69 Numeric character reference end state) permits arbitrary leading zeros. Browsers consume all digits until ;, decoding correctly. A payload like java:script:alert(1) slips through because the entity isn’t matched or replaced. This differs from CVE-2026-31860 (attribute key injection via data-* prefix), targeting value decoding instead.

Fix requires greedy regexes (e.g., /&#x([0-9a-f]+);?/gi) followed by safe integer parsing to handle overflow or invalid code points. Unbounded matching risks DoS via huge numbers, so cap total length or parse iteratively.

Reproduction and Scope

Tested on Nuxt 4.x (current as of report), unhead 2.1.12, Node 20 LTS, Chrome 146+. Steps:

  1. Init project:
    npx nuxi@latest init poc
    cd poc
    npm install
  2. Replace pages/index.vue with a useHeadSafe() call injecting a padded payload into a link href, e.g., "java:script:alert(1)".
  3. Run npm run dev, inspect SSR source (curl or view-page-source). Raw padded entity appears safe.
  4. Browser DOM shows decoded javascript:alert(1), triggering on click.

Affects any Nuxt app using useHeadSafe() for dynamic <head> like user avatars in link[rel="icon"] or OG images. Rare in practice—most head content is static—but critical if handling untrusted input.

Why This Matters and Disclosure Status

Nuxt pushes useHeadSafe() as the secure path for user content, implying protection against XSS vectors in SSR head tags. This bypass undermines that trust, exposing apps to reflected/stored XSS via protocol handlers. In finance/crypto sites (Njalla’s wheelhouse), it could leak wallets or sessions if chained with social engineering. Impact scores medium (CVSS ~6.5): requires user interaction, confined to head context, but evades touted safeguards.

Reported to Vercel HackerOne March 22, 2026—no response after 12 days. Cross-posted publicly April 3, 2026. Unhead/Nuxt teams should patch ASAP; devs, audit useHeadSafe() usage or strip entities manually with a robust decoder like he library. Skeptically, fixed-width regexes smell like premature optimization—HTML parsing demands spec fidelity over shortcuts.

Broader lesson: SSR security hinges on matching browser behavior exactly. Entity decoding isn’t trivial; libraries often cut corners. If you’re building with Nuxt/Vue, test head injections rigorously. Until fixed, avoid useHeadSafe() for untrusted data—default to server-side validation.

April 10, 2026 · 3 min · 11 views · Source: GitHub Security

Related