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. See the Security guide for the wider security model.