---
title: "Security headers and CSP in Nuxt with nuxt-security · vuejs.school"
description: "Add nuxt-security to a Nuxt 4 app, read the headers and the Content Security Policy it sends, allow the origins you need and test every change with curl."
canonical_url: "https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security"
meta:
  "og:description": "Add nuxt-security to a Nuxt 4 app, read the headers and the Content Security Policy it sends, allow the origins you need and test every change with curl."
  "og:image": "https://vuejs.school/_og/d/a_~U2VjdXJpdHkgaGVhZGVycyBhbmQgQ1NQIGluIE51eHQgd2l0aCBudXh0LXNlY3VyaXR5IMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_Security%2Bheaders%2Band%2BCSP%2Bin%2BNuxt%2Bwith%2Bnuxt-security%2Cdescription_~QWRkIG51eHQtc2VjdXJpdHkgdG8gYSBOdXh0IDQgYXBwLCByZWFkIHRoZSBoZWFkZXJzIGFuZCB0aGUgQ29udGVudCBTZWN1cml0eSBQb2xpY3kgaXQgc2VuZHMsIGFsbG93IHRoZSBvcmlnaW5zIHlvdSBuZWVkIGFuZCB0ZXN0IGV2ZXJ5IGNoYW5nZSB3aXRoIGN1cmwu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZWN1cml0eS1oZWFkZXJzLWFuZC1jc3Atd2l0aC1udXh0LXNlY3VyaXR5Ig%2Cs_KH1SfoCcaP4iiS-H.png"
  "og:image:alt": "Security headers and CSP in Nuxt with nuxt-security · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Security headers and CSP in Nuxt with nuxt-security · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~U2VjdXJpdHkgaGVhZGVycyBhbmQgQ1NQIGluIE51eHQgd2l0aCBudXh0LXNlY3VyaXR5IMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_Security%2Bheaders%2Band%2BCSP%2Bin%2BNuxt%2Bwith%2Bnuxt-security%2Cdescription_~QWRkIG51eHQtc2VjdXJpdHkgdG8gYSBOdXh0IDQgYXBwLCByZWFkIHRoZSBoZWFkZXJzIGFuZCB0aGUgQ29udGVudCBTZWN1cml0eSBQb2xpY3kgaXQgc2VuZHMsIGFsbG93IHRoZSBvcmlnaW5zIHlvdSBuZWVkIGFuZCB0ZXN0IGV2ZXJ5IGNoYW5nZSB3aXRoIGN1cmwu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZWN1cml0eS1oZWFkZXJzLWFuZC1jc3Atd2l0aC1udXh0LXNlY3VyaXR5Ig%2Cs_KH1SfoCcaP4iiS-H.png"
  "twitter:image:alt": "Security headers and CSP in Nuxt with nuxt-security · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Install the module and read its defaults

One command installs the package and adds it to `modules` in `nuxt.config.ts`:

```
npx nuxt module add security
```

With no configuration at all, the module registers server middleware and route rules that apply to every response. Among other things, it sends:

- `Content-Security-Policy`, covered in the next section.
- `Strict-Transport-Security: max-age=15552000; includeSubDomains`, which tells browsers to use HTTPS only, for 180 days.
- `X-Frame-Options: SAMEORIGIN` and a `frame-ancestors 'self'` directive, so other sites cannot put your pages in a frame.
- `Referrer-Policy: no-referrer`, so other sites never learn which page a visitor came from.
- `X-Content-Type-Options: nosniff`, cross-origin isolation headers and a `Permissions-Policy` that turns off the camera, microphone, geolocation, fullscreen and screen capture.

It also hides the `X-Powered-By` header, removes `console` calls and `debugger` statements from production builds, limits request bodies to 2 MB (8 MB for uploads) and sets a rate limit of 150 requests per IP address every five minutes. The defaults are chosen so that a typical app keeps working, which is why some of them are looser than they could be.

## How the default Content Security Policy works

A Content Security Policy (CSP) is a list of rules that says where scripts, styles, images and other resources may come from. If an attacker manages to inject a `<script>` tag, the browser refuses to run it because it breaks the rules. The module's default `script-src` directive has five sources: `'self' https: 'unsafe-inline' 'strict-dynamic' 'nonce-{{nonce}}'`.

On each server-rendered request, the module generates a random nonce, a one-time token, puts it in place of `{{nonce}}` and adds the same value to every `<script>` tag Nuxt renders. A script without that token does not run. `'strict-dynamic'` then lets those trusted scripts load others, which is how Nuxt's entry script can pull in route chunks during hydration. Browsers that understand `'strict-dynamic'` ignore `'self'`, `https:` and `'unsafe-inline'`; those three are only fallbacks for old browsers.

The nonce exists only when the server renders the page. With `nuxt generate`, the module switches to hashes of each inline script and writes the policy into a `<meta>` tag instead, because there is no server to send a header.

`style-src` is the one directive where the default `'unsafe-inline'` still takes effect. Vue writes `style` attributes for `:style` bindings, and Nuxt can insert styles during hydration, which `'strict-dynamic'` cannot authorise because it only applies to scripts. Leave styles as they are, and do not add a nonce to `style-src`: a nonce switches `'unsafe-inline'` off.

One more default helps a Vue app without costing anything. `script-src-attr 'none'` blocks inline handlers such as `onclick="..."`. Vue's `@click` is attached with `addEventListener`, so it is not affected, but an `onclick` hidden in content rendered with `v-html` is.

## Allow the origins you need

The first thing most apps hit is an image from another domain, because the default `img-src` is `'self' data:`. Add the origin in `nuxt.config.ts`:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  modules: ['nuxt-security'],
  security: {
    headers: {
      contentSecurityPolicy: {
        'img-src': ["'self'", 'data:', 'https://images.example.com'],
        'connect-src': ["'self'", 'https://api.example.com'],
        'frame-ancestors': ["'none'"],
      },
      xFrameOptions: 'DENY',
    },
  },
})
```

Objects merge with the defaults, so every directive you do not mention keeps its default value. Arrays replace them, which is why `'self'` and `data:` are repeated in `img-src`; leave them out and your own images stop loading.

`connect-src` controls where `fetch` and `$fetch` may send requests. The default policy does not set it, so listing your API here is a tightening, not a fix.

Scripts are different. With `'strict-dynamic'` in place, adding a domain to `script-src` does nothing, because the browser ignores host lists. Load third-party scripts with `useScript()` instead: Nuxt's own trusted script inserts them, and `'strict-dynamic'` lets them through. `useScript()` comes from the Nuxt Scripts module (`npx nuxt module add @nuxt/scripts`); without it, Nuxt 4.4 auto-imports a placeholder that throws an error in the browser.

On a live site, try each new directive in report-only mode before you enforce it, as the last section shows.

## Choose the framing, HTTPS and referrer settings

The config above also tightens framing. `frame-ancestors` is the modern CSP directive, and `'none'` means no site, not even yours, may frame the page. `X-Frame-Options: DENY` says the same to browsers that predate it. Keep the default `'self'` and `SAMEORIGIN` only if your app frames its own pages.

Keep the HSTS (`Strict-Transport-Security`) default. Browsers ignore the header on plain HTTP, including `http://localhost`, so it cannot lock you out during development. Add `preload: true` only when every subdomain serves HTTPS; the docs note that preloading needs a `maxAge` of at least 31536000 (one year) and `includeSubdomains`.

`no-referrer` is the most private referrer setting. If a partner site needs to see that visitors came from you, switch to `referrerPolicy: 'strict-origin-when-cross-origin'`, which sends other sites only your origin, never the full path, and sends nothing when a link leads from HTTPS to HTTP.

## Limit request rates and sizes on API routes

The global limits are generous on purpose. Tighten them on the routes that need it with `routeRules`, in the same `nuxt.config.ts`:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  routeRules: {
    '/api/login': {
      security: {
        rateLimiter: { tokensPerInterval: 5, interval: 60_000 },
      },
    },
    '/api/avatar': {
      security: {
        requestSizeLimiter: { maxUploadFileRequestInBytes: 2_000_000 },
      },
    },
  },
})
```

The login route now accepts five attempts a minute from one IP address and answers the next one with `429 Too Many Requests`. It keeps its own count, separate from the global one, and the minute is counted from the first attempt. One exception in nuxt-security 2.5.1: the very first request from an address only creates its record and uses up no attempt, so that address's first minute lets a sixth attempt through. The avatar route rejects uploads over 2 MB with `413 Payload Too Large`.

Before you rely on these limits:

- **Storage.** The default store is an in-memory cache, so each server instance counts separately. On serverless hosting, set a shared unstorage `driver` in `security.rateLimiter`.
- **The client's address.** The limiter reads it from the first `X-Forwarded-For` entry, which any client can set unless your proxy overwrites it. If your host passes the real address in its own header, such as Cloudflare's `cf-connecting-ip`, set `ipHeader` to it. If the proxy passes no address at all, every visitor shares one limit.
- **Request size.** The check reads `Content-Length` on POST, PUT and DELETE requests, so a chunked request without that header, or a PATCH, gets through. Check an upload's size in its handler too.

For a complex production app, the module's docs recommend protection against brute forcing at the infrastructure layer instead.

## Check the headers with curl and the browser

Configuration describes what you meant. The response shows what happened. Build the app and run the production server, because some headers, such as the cross-origin embedder policy, are relaxed in development:

```
npx nuxt build
node .output/server/index.mjs
# in a second terminal: a GET request that prints only the response headers
curl -s -D - -o /dev/null http://localhost:3000/
```

Look for `content-security-policy` with a fresh `'nonce-...'` value on every request, `x-frame-options: DENY`, and `strict-transport-security`. Then open the page in the browser, open the developer tools, and select the document request in the Network panel to see the same headers. The Console is where a policy shows its effect: each blocked resource prints a message saying it was refused because it violates a named directive.

## Try changes in report-only mode first

A wrong policy breaks a page silently for every visitor. So before you enforce a new rule, ship it as `Content-Security-Policy-Report-Only`. The browser evaluates that header, logs every violation, and blocks nothing. The module does not build this header for you; set it as a plain route header, as its FAQ shows:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  routeRules: {
    '/**': {
      headers: {
        'Content-Security-Policy-Report-Only':
          "img-src 'self' data: https://images.example.com; connect-src 'self' https://api.example.com; frame-src https://www.youtube-nocookie.com",
      },
    },
  },
})
```

Browse every part of the app, watch the Console for `[Report Only]` messages, and adjust. Change one directive at a time, so each message points to one change. When a full pass produces none, move the directives into `security.headers.contentSecurityPolicy`, delete the report-only header, and check the enforced policy with curl.

**Check it stuck**3 questions from the Security misconfiguration, headers and CSP quiz.

[**Practice now**](https://vuejs.school/learn/security/security-headers-and-csp?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 7 min read**API security for AI features: rate limits, prompt injection and error handling**](https://vuejs.school/community/blog/api-security-for-ai-features) [Tutorial · 8 min read**Supply chain security: lockfiles, provenance and OWASP A03**](https://vuejs.school/community/blog/supply-chain-security-lockfiles-provenance-and-owasp-a03)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-04-14","description":"Add nuxt-security to a Nuxt 4 app, read the headers and the Content Security Policy it sends, allow the origins you need and test every change with curl.","name":"Security headers and CSP in Nuxt with nuxt-security","url":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security","breadcrumb":{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-04-14","description":"Add nuxt-security to a Nuxt 4 app, read the headers and the Content Security Policy it sends, allow the origins you need and test every change with curl.","headline":"Security headers and CSP in Nuxt with nuxt-security","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~U2VjdXJpdHkgaGVhZGVycyBhbmQgQ1NQIGluIE51eHQgd2l0aCBudXh0LXNlY3VyaXR5IMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_Security%2Bheaders%2Band%2BCSP%2Bin%2BNuxt%2Bwith%2Bnuxt-security%2Cdescription_~QWRkIG51eHQtc2VjdXJpdHkgdG8gYSBOdXh0IDQgYXBwLCByZWFkIHRoZSBoZWFkZXJzIGFuZCB0aGUgQ29udGVudCBTZWN1cml0eSBQb2xpY3kgaXQgc2VuZHMsIGFsbG93IHRoZSBvcmlnaW5zIHlvdSBuZWVkIGFuZCB0ZXN0IGV2ZXJ5IGNoYW5nZSB3aXRoIGN1cmwu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZWN1cml0eS1oZWFkZXJzLWFuZC1jc3Atd2l0aC1udXh0LXNlY3VyaXR5Ig%2Cs_KH1SfoCcaP4iiS-H.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/security-headers-and-csp-with-nuxt-security#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Security headers and CSP in Nuxt with nuxt-security","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~U2VjdXJpdHkgaGVhZGVycyBhbmQgQ1NQIGluIE51eHQgd2l0aCBudXh0LXNlY3VyaXR5IMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_Security%2Bheaders%2Band%2BCSP%2Bin%2BNuxt%2Bwith%2Bnuxt-security%2Cdescription_~QWRkIG51eHQtc2VjdXJpdHkgdG8gYSBOdXh0IDQgYXBwLCByZWFkIHRoZSBoZWFkZXJzIGFuZCB0aGUgQ29udGVudCBTZWN1cml0eSBQb2xpY3kgaXQgc2VuZHMsIGFsbG93IHRoZSBvcmlnaW5zIHlvdSBuZWVkIGFuZCB0ZXN0IGV2ZXJ5IGNoYW5nZSB3aXRoIGN1cmwu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZWN1cml0eS1oZWFkZXJzLWFuZC1jc3Atd2l0aC1udXh0LXNlY3VyaXR5Ig%2Cs_KH1SfoCcaP4iiS-H.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~U2VjdXJpdHkgaGVhZGVycyBhbmQgQ1NQIGluIE51eHQgd2l0aCBudXh0LXNlY3VyaXR5IMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_Security%2Bheaders%2Band%2BCSP%2Bin%2BNuxt%2Bwith%2Bnuxt-security%2Cdescription_~QWRkIG51eHQtc2VjdXJpdHkgdG8gYSBOdXh0IDQgYXBwLCByZWFkIHRoZSBoZWFkZXJzIGFuZCB0aGUgQ29udGVudCBTZWN1cml0eSBQb2xpY3kgaXQgc2VuZHMsIGFsbG93IHRoZSBvcmlnaW5zIHlvdSBuZWVkIGFuZCB0ZXN0IGV2ZXJ5IGNoYW5nZSB3aXRoIGN1cmwu%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zZWN1cml0eS1oZWFkZXJzLWFuZC1jc3Atd2l0aC1udXh0LXNlY3VyaXR5Ig%2Cs_KH1SfoCcaP4iiS-H.png"}]}
```