---
title: "Security"
description: "Escape behavior, untrusted input, CSP compatibility, nonces, SRI, and privacy defaults for scripts loaded through Unhead."
canonical_url: "https://unhead.unjs.io/docs/head/guides/core-concepts/security"
last_updated: "2026-08-27T17:08:12.833Z"
---

Unhead renders the tags you give it. It escapes what it can, but it does not decide for you what is safe to render. This guide covers where Unhead helps, where it steps back, and what to expect under a Content Security Policy.

## Untrusted input

Head data often comes from somewhere else: a CMS, a user profile, an API response. The dangerous case is passing such data as a whole head object, because an attacker who controls the JSON controls the tags:

```ts
// profile.head comes from an API the user can edit
useHead(profile.head)
// { script: [{ src: 'https://evil.example.com/x.js' }] }
// → arbitrary script, exactly as the attacker wrote it
```

Unhead renders whatever that object contains. Filtering has to happen first. Pass untrusted input through [`useHeadSafe()`](/docs/head/api/composables/use-head-safe): it keeps an allowlist of tags and attributes, drops executable scripts, and blocks URL schemes such as `javascript:`.

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

useHeadSafe(profile.head)
// script dropped, data-* dropped, javascript: URLs rejected
```

For metadata your application owns, `useHead()` and `useSeoMeta()` are the right tools. Nothing extra is needed.

## Escaping

During SSR, Unhead encodes attribute values and escapes `title` text for you. Inline content in `script`, `style`, and `noscript` tags is a different story: it is serialized raw.

Unhead does escape the closing tag sequence, so content cannot break out of its element. But the code itself runs exactly as written. If you put untrusted JavaScript or CSS there, it executes. Treat `textContent` and `innerHTML` on scripts and styles as code you authored yourself. The [Inline Style & Scripts](/docs/head/guides/core-concepts/inner-content) guide explains the parsing contexts.

## Content Security Policy

### Event handlers

On the client, Unhead never writes inline event handlers. Every `onload`, `onerror`, and `bodyAttrs` listener is attached with `addEventListener()`. A policy with `script-src-attr 'none'` produces no violations from client-side rendering.

SSR is the one exception. A function handler is serialized as a marker attribute:

```html
<script onload="this.dataset.onloadfired = true"></script>
```

The marker exists so Unhead can replay a handler that fired before hydration. A strict policy blocks it and logs a violation to the console. That is the full extent of the damage: the resource still loads, and client-side handlers keep working after hydration. See [DOM Event Handling](/docs/head/guides/core-concepts/dom-event-handling).

### Inline scripts you author

`nonce` passes through on `script` and `style` tags. Generate a fresh nonce per response, add it to the tags, and send the same value in the `Content-Security-Policy` header. If you use Nuxt, the [Nuxt Security](https://nuxt-security.vercel.app/) module can generate per-request nonces for you.

```ts
useHead({
  script: [
    { textContent: 'window.dataLayer = window.dataLayer || []', nonce: requestNonce },
  ],
})
```

### Streaming

SSR streaming injects a small inline bootstrap script plus per-chunk update scripts. The framework streaming options expose only `mode`. The low-level unplugin accepts a `nonce`, string or per-request function, but it covers just the Vite-injected client bootstrap. Streamed chunk updates carry no nonce. A nonce-only policy needs the manual template-free flow in the [Streaming guide](/docs/typescript/head/guides/core-concepts/streaming): `createBootstrapScript()` takes your nonce there, and you stamp the update scripts yourself.

### Policy meta tag

A `<meta http-equiv="content-security-policy">` tag is supported. Unhead sorts it at weight -30, ahead of most other tags. Prefer a header-based policy where you can. The meta form cannot express `frame-ancestors` or report-only mode.

## Third-party scripts

[`useScript()`](/docs/head/api/composables/use-script) ships conservative defaults for external scripts:

- `crossorigin="anonymous"` and `referrerpolicy="no-referrer"` for absolute or protocol-relative URLs
- `defer` and `fetchpriority="low"`, so third-party code is less likely to hold up your critical resources. Actual scheduling still depends on the browser.

Add `integrity` to pin a script or stylesheet with Subresource Integrity (SRI). Generate the digest from the exact file you deploy, for example `openssl dgst -sha384 -binary sdk.js | openssl base64 -A`:

```ts
useScript('https://cdn.example.com/sdk.js', {
  integrity: 'sha384-YOUR_BASE64_DIGEST',
})
```

To load a script only after consent, hold it back with a trigger and call `load()` when consent is granted:

```ts
const analytics = useScript('https://example.com/analytics.js', {
  trigger: 'manual',
})

// later, in your consent callback:
analytics.load()
```

Interaction triggers work well here too: load on first click or keypress rather than on page load.

## Checklist

- Filter untrusted head data with `useHeadSafe()`
- Treat inline `textContent` and `innerHTML` as authored code
- Serve inline scripts with per-response nonces
- Add `integrity` hashes where the CDN supports SRI
- Expect a console violation per SSR handler under `script-src-attr 'none'`. Replay is the only thing lost.

## See Also

- [useHeadSafe() API](/docs/head/api/composables/use-head-safe): Allowlist reference
- [Inline Style & Scripts](/docs/head/guides/core-concepts/inner-content): Inline content behavior
- [DOM Event Handling](/docs/head/guides/core-concepts/dom-event-handling): Handler rendering and CSP notes
- [Loading Scripts](/docs/head/guides/core-concepts/loading-scripts): Triggers and warmup
