Composables

useHeadSafe()

Last updated by Harlan Wilton in doc: clean up.

useHeadSafe() uses the same entry lifecycle as useHead(), but filters each resolved tag through a restrictive allowlist. Use it when head data comes from users, a CMS, or another source you do not fully trust.

import { useHeadSafe } from '@unhead/vue'

const entry = useHeadSafe({
  title: userInput.title,
  meta: [{ name: 'description', content: userInput.description }],
})

It returns an ActiveHeadEntry with patch() and dispose() methods.

Allowlist

The safe-input plugin keeps only the following attributes. Valid data-* attributes are also allowed, and id is allowed on tags other than htmlAttrs and bodyAttrs.

InputAllowed attributes
htmlAttrsclass, style, lang, dir
bodyAttrsclass, style
metaname, property, charset, content, media
linkcolor, crossorigin, fetchpriority, href, hreflang, imagesrcset, imagesizes, integrity, media, referrerpolicy, rel, sizes, type
stylemedia, nonce, title, blocking
scripttype, nonce, blocking

Titles are retained and escaped when rendered. Inline style and noscript content are removed.

JSON scripts

Executable scripts are rejected. A script is kept only when it has textContent and its type ends in json, such as application/json or application/ld+json.

useHeadSafe({
  script: [{
    type: 'application/json',
    textContent: { theme: 'dark' },
  }],
})

The JSON is parsed or serialized again, and prototype-related keys are removed. innerHTML is never allowed in safe mode.

A link must have a rel plus either href or imagesrcset. URLs using javascript:, data:, or vbscript: are rejected, including encoded variants.

The following high-impact link relations are blocked because they can change document identity, start speculative fetching, or attach application metadata:

[
  'canonical',
  'modulepreload',
  'prerender',
  'preload',
  'prefetch',
  'dns-prefetch',
  'preconnect',
  'manifest',
  'pingback',
]

http-equiv is not allowed on meta tags, and event-handler attributes are not allowed on any safe tag.

API

function useHeadSafe(
  input?: HeadSafe,
  options?: HeadEntryOptions,
): ActiveHeadEntry<HeadSafe>

Unsupported tags and attributes are silently dropped. The remaining entry still participates in normal resolution, deduplication, and framework lifecycle cleanup.

Handling untrusted data

import { useHeadSafe } from '@unhead/vue'

const profile = await fetchUserProfile(userId)

useHeadSafe({
  title: profile.pageTitle,
  meta: [
    { name: 'description', content: profile.pageDescription },
    ...profile.customMetaTags,
  ],
})

Security notes

The allowlist reduces the attack surface, but it should be one layer in your security model:

  • Validate that input is a plain object with the expected primitive value types before calling useHeadSafe(). Custom coercion methods can throw during normalization, before the allowlist runs. See the OWASP Input Validation Cheat Sheet.
  • Validate data as close to its source as possible.
  • Use a Content Security Policy as an additional defense layer.
  • Treat style values on htmlAttrs and bodyAttrs as untrusted CSS and validate them separately if your application permits them.
  • Use useHead() only after sanitizing any fields that the safe API intentionally rejects.

For ordinary, typed SEO metadata, useSeoMeta() is usually more convenient.

Did this page help you?