useHeadSafe()
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.
| Input | Allowed attributes |
|---|---|
htmlAttrs | class, style, lang, dir |
bodyAttrs | class, style |
meta | name, property, charset, content, media |
link | color, crossorigin, fetchpriority, href, hreflang, imagesrcset, imagesizes, integrity, media, referrerpolicy, rel, sizes, type |
style | media, nonce, title, blocking |
script | type, 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.
Links
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
stylevalues onhtmlAttrsandbodyAttrsas 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.