---
title: "useHeadSafe()"
description: "Filter untrusted head input through a restrictive tag and attribute allowlist with useHeadSafe()."
canonical_url: "https://unhead.unjs.io/docs/head/api/composables/use-head-safe"
last_updated: "2026-08-11T00:40:47.285Z"
---

`useHeadSafe()` uses the same entry lifecycle as [`useHead()`](/docs/head/api/composables/use-head), 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.

```ts
import { useHeadSafe } from '@unhead/dynamic-import'

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`.

<table>
<thead>
  <tr>
    <th>
      Input
    </th>
    
    <th>
      Allowed attributes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        htmlAttrs
      </code>
    </td>
    
    <td>
      <code>
        class
      </code>
      
      , <code>
        style
      </code>
      
      , <code>
        lang
      </code>
      
      , <code>
        dir
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        bodyAttrs
      </code>
    </td>
    
    <td>
      <code>
        class
      </code>
      
      , <code>
        style
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        meta
      </code>
    </td>
    
    <td>
      <code>
        name
      </code>
      
      , <code>
        property
      </code>
      
      , <code>
        charset
      </code>
      
      , <code>
        content
      </code>
      
      , <code>
        media
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        link
      </code>
    </td>
    
    <td>
      <code>
        color
      </code>
      
      , <code>
        crossorigin
      </code>
      
      , <code>
        fetchpriority
      </code>
      
      , <code>
        href
      </code>
      
      , <code>
        hreflang
      </code>
      
      , <code>
        imagesrcset
      </code>
      
      , <code>
        imagesizes
      </code>
      
      , <code>
        integrity
      </code>
      
      , <code>
        media
      </code>
      
      , <code>
        referrerpolicy
      </code>
      
      , <code>
        rel
      </code>
      
      , <code>
        sizes
      </code>
      
      , <code>
        type
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        style
      </code>
    </td>
    
    <td>
      <code>
        media
      </code>
      
      , <code>
        nonce
      </code>
      
      , <code>
        title
      </code>
      
      , <code>
        blocking
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        script
      </code>
    </td>
    
    <td>
      <code>
        type
      </code>
      
      , <code>
        nonce
      </code>
      
      , <code>
        blocking
      </code>
    </td>
  </tr>
</tbody>
</table>

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`.

```ts
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:

```ts
[
  '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

```ts
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

```ts
import { useHeadSafe } from '@unhead/dynamic-import'

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](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html).
- Validate data as close to its source as possible.
- Use a [Content Security Policy](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) 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()`](/docs/head/api/composables/use-seo-meta) is usually more convenient.
