Strict CSP

An advanced guide on Strict Content Security Policy


Foreword

This section is provided as a guide to help you understand the implications of a Strict CSP for your Nuxt application.

Strict CSP is known to be hard to implement, especially for JS frameworks. You can read more about Strict CSP here.

Our recommended values provide sensible defaults that will enable Strict CSP in your application with minimum effort.

However you may encounter situations where you want to finetune your settings, which may imply some level of code refactoring. This guide walks you through the elements that you need to take into account.

Useful links on CSP:

The CSP challenges

For modern Javascript frameworks such as Nuxt, a proprer implementation of CSP needs to solve three different challenges simultaneously:

Resource Control

The goal of CSP is to prevent your site from loading unauthorized scripts and stylesheets.

In most cases, you don't have significant control over the resources that Nuxt is trying to load.

Sometimes, you don't even know which resources are being loaded, and how they are being loaded (external or inlined), unless you start digging really deep to find out.

The first step when designing your own CSP rules is therefore to identify which resources you want to authorize.

Hydration

Nuxt is designed as an isomorphic framework, which means that HTML is rendered first server-side, and then client-side via the hydration mechanism.

While Nuxt Security can make a lot of the heavy-lifting to ensure everything is fine on the server-side, hydration is a mechanism by which the browser will sometimes inject scripts and styles in your application.

Because CSP is designed to prevent scripts and styles injection, hydration requires carefully-crafted policies.

The second step is to design rules that do not block hydration.

Rendering Mode

Nuxt is an SSR-first framework, but also provides support for SSG mode.

In SSR mode, Nuxt is in charge serving your application (via the Nitro server), and therefore can control how the CSP policies are delivered. However in SSG mode, your files are delivered by your own static provider, so we cannot leverage Nitro to control CSP.

This is why Nuxt Security uses different mechanisms for SSR (nonces) and SSG (hashes).

The last step is to take into account the differences between SSR and SSG.

Inline vs. External

CSP treats inline elements and external resources differently.

Inline elements

Inline elements are elements which are directly inserted in your HTML. Examples include:

<!-- An inlined script -->
<script>console.log('Hello World')></script>
<!-- An inlined style -->
<style>h1 { color: blue }</style>

External resources

External resources are elements that are loaded from a server. Examples include:

<!-- An image loaded from the example.com domain -->
<img src="https://example.com/my-image.png">
<!-- A script loaded from your origin, this is still an 'external' resource -->
<script src="/_nuxt/entry.065a09b.js" />
<!-- A stylesheet loaded from a CDN -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" />

Why they differ

The difference between external and inlined elements is important in CSP terminology because of 2 reasons:

1. The danger is not the same.

  • An external resource can be dangerous because it can load a malicious script (e.g. <script src="https://evildomain.com/malicious.js">).
    CSP has a simple and efficient mechanism against this issue: you need to whitelist your external resources.
    If you set your CSP policy as script-src https://example.com, the script from https://evildomain.com will be blocked because evildomain is not in the whitelist.
  • An inline resource is more dangerous because it can lead to XSS injection. Your code could be vulnerable to an inline script injection such as <script>doSomethingEvil()</script>.
    For inline elements, CSP has no easy way to determine whether they are legit or not.
    Therefore CSP adopts a different (and quite brutal) first-line of defense against this issue: it forbids all inline elements by default.

2. The problem for Nuxt is not the same

  • Nuxt applications typically insert lots of external and inline elements in a dynamic manner.
    While the insertion of external resources is not a huge problem (they can be whitelisted), the insertion of inline elements is a real issue with Nuxt because CSP then blocks the whole application.
In summary, external elements are manageable via whitelisting, but inline elements are a true challenge for Nuxt. We need a mechanism to tell CSP which inline elements are legit and which are not.

Allowing inline elements for Nuxt

Fortunately, CSP provides several mechanisms to allow the dynamic insertion of legit inline elements.

The available mechanisms depend on the CSP version that you are using. There are three versions available, the most recent being CSP Level 3.

CSP Level 3 is supported by most modern browsers and should be the default approach. You can check adoption on caniuse

Each level is backwards-compatible with the previous ones, meaning that

  • If your browser supports Level 3, you can still write Level 1 policies
  • If your browser only supports Level 2, it will ignore Level 3 syntax

'unsafe-inline' (CSP Level 1)

In the oldest version (CSP Level 1), the only way to allow inline elements was to use the 'unsafe-inline' directive.

This meant that all inline elements were allowed. This disabled the protection that CSP offered against inline injection, but it was the only way to make Nuxt work under CSP.

In other words, CSP Level 1 has a binary approach to inline elements: either they are all denied, or they are all allowed.

Obviously, allowing all unsafe inline scripts is not recommended: as it name implies, it is unsafe. But as a last resort and if everything else fails, you still have this option.

Using 'unsafe-inline' is unsafe and therefore not recommended.
However CSP Level 2 has safe alternatives.
See below how you can indicate which inline elements should be allowed.

Nonces and Hashes (CSP Level 2)

CSP Level 2 introduced two mechanisms to determine which inline elements are allowed: nonces and hashes.

Nonces

A nonce is a unique random number which is generated by the server for each request. This nonce is added to each inline element and when the response arrives, the browser will see :

<script nonce="somerandomstring">console.log('Hello World')</script>

The server also sends the nonce in the HTTP headers:

Content-Security-Policy: script-src 'nonce-somerandomstring'

Now the browser can compare the two nonce values to determine that the inline element is valid.

Hashes

A hash is the SHA hash of the inline code. Hashes are not added to the inline elements, but the browser can hash the content of the inline script itself:

<script>console.log('Hello, World !!')</script>
<!--    [Take that string and hash it]      -->

The server sends the hashed value in the HTTP headers:

Content-Security-Policy: script-src 'sha256-......'

Now the browser can compare the hash received in the headers with the value it computed itself to determine that the inline element is valid.

With CSP Level 2, we can now indicate which inline elements should be allowed and which ones should be rejected.

Implications for Nuxt

However, there are many important details that you should know:

  1. Hashes and Nonces cancel 'unsafe-inline'. In other words, all inline elements must have a nonce or hash.
    Therefore for Nuxt applications to work, it is critical that every single inline element is included. If one inline element is omitted, it will be blocked.
  2. Hashes and Nonces are primarily intended for inline elements. External resources are still supposed to be whitelisted the old way, i.e. by including the domain name or file name in the policy. However, CSP Level 2 added the option to also whitelist external resources by nonce, but not by hash. So:
    • If you use nonce: Inline and external elements can be whitelisted by nonce. External elements can also be whitelisted by name.
    • If you use hash: Inline elements can be whitelisted by hash. External elements still need to be whitelisted by name.
  3. Hashes and Nonces only work on scripts and styles. It is useless to use them on other tags (<img>, <frame>, <object> etc.), or to inlude them in any policy other than script-src and style-src.
    A common mistake is to try to whitelist an external image by nonce via the img-src policy: this will not work.
This has several critical implications for Nuxt:
  1. SSR vs SSG: Using SSR is easier because the nonce whitelists both inline and external elements. But if you use SSG, you will still need to whitelist external elements by name.
  2. Hydration: This problem remains largely unsolved, because now all inline elements must have a nonce or hash. If the client-side hydration mechanism tries to insert an element, that element will be blocked:
    • All inserted inline elements will be blocked;
    • Inserted external elements will be blocked, except if you have whitelisted them by name (even in the SSR case)

Therefore, a Level 2 configuration file should look like:

export defaultNuxtConfig({
  security: {
    headers: {
      contentSecurityPolicy: {
        "script-src": [
          "'nonce-{{nonce}}'",
          // nonce will allow inline scripts that are inserted server-side
          // But the application will block if client-side hydration tries to insert a script 
          "https:example.com" 
          // example.com must still be whitelisted by name to support SSG
          // example.com must still be whitelisted by name to support hydration
        ]
      }
    }
  }
})

Some simple Nuxt applications will be able to operate under that scheme, but as you can see, hydration constraints defeat most of the solutions brought by CSP Level 2.

CSP Level 2 Nonces and Hashes will be uneffective for most Nuxt applications.
However they lay the foundation for the real solution which is CSP Level 3.
See below how you can unblock hydration on the client-side.

'strict-dynamic' (CSP Level 3)

Hydration solved

CSP Level 3 was designed by folks at Google who were facing the problems described above, and who came up with a solution: 'strict-dynamic'.

What 'strict-dynamic' does, is it allows a pre-authorized parent script to insert any child script. If the parent script is approved by its nonce or hash, then all children scripts do not need to carry a nonce or hash anymore.

For Nuxt, this solves the hydration problem, because Nuxt Security pre-authorizes your root script (the entry script).

So when hydration time comes, the Nuxt root script can now insert any inline or external script.

Important details

However, before you rush to this miraculous solution, you need to know the additional limitations that came with 'strict-dynamic':

  1. 'strict-dynamic' only works for the 'script-src' policy. A common mistake is to try to set 'strict-dynamic' on the style-src policy: it will not work.
  2. 'strict-dynamic' can only authorize scripts. In other words, a script inserted by Nuxt will be allowed, but a style inserted by Nuxt will be rejected.
    In practice, this means that if your Nuxt application is dynamically modifying styles, you will need to go back to Level 1 with 'unsafe-inline' for styles, and in that case you will need to be careful not to add any nonce or hash to the style-src policy (because, remember, nonces and hashes cancel 'unsafe-inline').
    Allowing 'unsafe-inline' for styles is widely considered as acceptable. Known exploits are not common and center around the injection of image urls in CSS. This risk can be eliminated if you set the img-src policy properly.
  3. 'strict-dynamic' cancels external whitelists. It is not possible to allow external scripts by name anymore if you use 'strict-dynamic'. Old-type external whitelists such as 'self' https://example.com are simply ignored when 'strict-dynamic' is used.
    In pratice, this means that external scripts must either:
    • be inserted by an authorized parent ('strict-dynamic' enters into play);
    • or be individually whitelisted by nonce (as in CSP Level 2) or integrity hash (a new CSP Level 3 feature).

    In Nuxt 3.11+, the easiest way to insert an external script is to do it via useScript. See our section below on the useScript composable
    If you are using Nuxt before version 3.11, you can also use the useHead composable.

In summary

In summary, Nuxt applications are perfectly capable of running with Strict CSP under two conditions:
  1. You use 'strict-dynamic' on the script-src policy
  2. You use 'useScript' to insert your external scripts

With this setup, a Level 3 configuration file is much simpler:

export defaultNuxtConfig({
  security: {
    headers: {
      contentSecurityPolicy: {
        "script-src": [
          "'nonce-{{nonce}}'",
          // The nonce allows the root script
          "'strict-dynamic'" 
          // All scripts inserted by the root script will also be allowed
        ]
      }
    }
  }
})

Whitelisting external resources

The useScript composable

Starting from Nuxt 3.11, it is possible to insert any external script in one single line with the new useScript composable.

useScript('https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js')

The useScript method has several key features:

  • It is an isomorphic function, i.e. you don't have to worry whether it is executed on the server-side or the client-side
  • It is loaded by the Nuxt root script, which means your script will always be loaded under 'strict-dynamic'
  • It does not insert inline event handlers, therefore CSP will never block the script from executing after load
  • It is designed to load and execute asynchronously, which means you don't have to write code to check whether the script has finished loading before using it

For all of these reasons, we strongly recommend useScript as the best way to load your external scripts in a CSP-compatible way.

The unjs/unhead repo has a detailed section here on how to use useScript.

Check out their examples and find out how easy it is to include Google Analytics in your application:

import { useScript } from 'unhead'

const { gtag } = useScript({
  src: 'https://www.google-analytics.com/analytics.js',
}, {
  use: () => ({ gtag: window.gtag })
})
// Now use any feature of Google's gtag() function as you wish
// Instead of writing complex code to find and check window.gtag

The useHead composable

For versions of Nuxt before 3.11, you can still use the older useHead approach to load your external scripts.

Compared to useScript, useHead provides a reduced set of features :

  • It is also an isomorphic function
  • It is also executed by the Nuxt root script, so your external script will be loaded under 'strict-dynamic'.

However:

  • It inserts inline event handlers, which means that execution could be denied after load
  • You will have to write custom code to determine when loading has finished if you need to extract globals from the window object

Despite these limitations, it is still possible to load external scripts with some minor tweaks:

  1. Include the external script via useHead
useHead({
  script: [
    // Insert any external script with strict-dynamic
    {
      src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
      crossorigin: 'anonymous'
    }
  ]
}, {
  mode: 'client' // Required only for SSG mode
})

Note: you will need to set the { mode: 'client' } option if you want to ensure compatibility with SSG mode.

  1. If required, modify your nuxt.config.ts file to allow the onload event handler:
defineNuxtConfig({
  security: {
    headers: {
      contentSecurityPolicy: {
        'script-src-attr': [
          "'unsafe-hashes'",
          "'sha256-jp2rwKRAEWWbK5cz0grQYZbTZyihHbt00dy2fY8AuWY='",
        ], // Allow the useHead 'onload' inline event handler
      }
    }
  }
})

Note: you will need to set the 'unsafe-hashes' values only if your script relies on the onload event to execute.

Debugging external resources

Because whitelisting of external resources can become difficult, especially in the SSG case, this section is here to help you find alternatives and solutions.

The Strict CSP Checklist

1. Did you keep all default options ?

For an optimal experience, we recommend that you stick with the default values of

  • the contentSecurityPolicy option
  • the nonce option
  • the ssg option

2. Are you using 'strict-dynamic' ?

If you have modified the "script-src" values of the contentSecurityPolicy option, please make sure that

  • you did not delete the 'strict-dynamic' or the 'nonce-{{nonce}}' values.
  • you did not disable the nonce or the ssg options.

If you cannot, or do not want to use 'strict-dynamic' mode, be prepared for things to potentially break under CSP. In that case, please see Without strict-dynamic below.

3. Do you need to execute the script on the server ?

We recommend that you insert your external resources with useScript.

This is necessary to ensure that your script is loaded by the client-side under 'strict-dynamic', and therefore executes in a CSP-compatible way.

In some rare cases however, you might want the server-side to load the script.

Please make sure you understand the implications first:

  • Are you sure the server needs to load the script ? Most external resources are designed to be executed on the client-side only, so this should normally be unnecessary.
  • Do you need to load your script via HTML tag insertion ? If your script is executed by the server, it is likely that you should have bundled your server-side script by way of a NPM module rather than loading it from a URL.
If you answered yes to both questions, then continue reading below.

4. Are you deploying with SSR ?

If this is the case, you are not supposed to encounter any issue because your script will be whitelisted by nonce.

With useScript, you can even run your script isomorphically on the server and on the client by enabling the { trigger: 'server' } option:

useScript({
  src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
  crossorigin: 'anonymous',
}, {
  trigger: 'server', // Use this option so that the script tag will be injected by the server,
  stub() {
    if (process.server) {
      // This code will be executed on the server
      // See documentation at: https://unhead.unjs.io/usage/composables/use-script#ssr-stubbing
    }
  }
})
In SSR mode, you will always be able to load your external scripts under Strict CSP, irrespective of the way you insert it.

5. Are you deploying with SSG ?

If this is the case, clearly you should not be seeking to execute the script on the server-side (you don't have a server...).

The universal solution is then to select client-only mode by including your script with useScript.

In SSG mode, you will also always be able to load your external scripts under Strict CSP, provided however that you use useScript.
If you have a specific requirement that prevents you from using useScript in client-only mode, please continue reading below.

6. In SSG, are you unable to use client-only mode ?

In other words, you have a specific requirement that forces you to use the server to inject the <script> tag, instead of letting the client insert the tag.

In that case, you will need to know the integrity hash of your external scripts.

With useScript you can write:

useScript({
  src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
  crossorigin: 'anonymous',
  integrity: 'sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL' // Add the integrity hash of your external script, so that it will execute on the client
}, {
  trigger: 'server' // This option will inject the script in the HTML static source
})
If you reached this point and you cannot determine the integrity hash of at least one of your external scripts, please read our Alternative Solutions section below

Alternative Solutions

  • Option 1: verify if there is an existing Nuxt module that already wraps your script:
npm install @bootstrap-vue-next/nuxt
export default defineNuxtConfig({
  modules: ['@bootstrap-vue-next/nuxt'],
  bootstrapVueNext: {
    composables: true, // Will include all composables
  },
  css: ['bootstrap/dist/css/bootstrap.min.css'],
})
  • Option 2: verify if your script can be installed via npm module rather than <script> tags:
npm install @stripe/stripe-js
<script setup> 
import { loadStripe } from '@stripe/stripe-js';

onMounted(async() => {
  const stripe = await loadStripe('pk_test_xxxxxxxx');
})
</script>
  • Option 3: wrap the script yourself in a client-side Nuxt plugin
import { useScript } from 'unhead';

export default defineNuxtPlugin((nuxtApp) => {
  const { $script } = useScript(
    {
      src: 'https://example.com/script.js',
      async: true,
      defer: true,
    }
  )

  return {
    provide: {
      script: $script
    },
  }
})

Your script will be loaded globally and available in your application via:

const { $script } = useNuxtApp()
  • Option 4: create a copy of the current version of script, hash it yourself and serve it locally
# Suppose you have made a local copy of the bootstrap javascript distributable
# Calculate the integrity hash yourself
cat bootstrap-copy.js | openssl dgst -sha384 -binary | openssl base64 -A
# Copy the file in the public folder of your Nuxt application
cp bootstrap-copy.js /my-project/public
useHead({
  script: [
    {
      src: '/bootstrap-copy.js', // Serve the copy from self-origin
      crossorigin: 'anonymous',
      integrity: 'sha384-.....' // The hash that you have calculated above
    }
  ]
})
In SSG mode, strict-dynamic requires extra care to ensure Strict CSP.
If all above alternatives have failed, you might have to abandon strict-dynamic

Without 'strict-dynamic'

If you cannot use 'strict-dynamic' for your app, you can still use the useHead composable.

In that case, you are going back to CSP Level 2, which means that you will need to take into account the following constraints:

1. Client-side Hydration might break

Because you don't have 'strict-dynamic' anymore, there is a possibility that Nuxt may seek to insert inline scripts on the client side that will get rejected by CSP.

It is not always the case though, so depending on how your application is designed, you might still get Strict CSP via nonces or hashes and a fully-functional application.

2. You will need to whitelist external scripts manually

  • If you are using SSR, Nuxt Security will do this for you automatically by inserting the nonce on the server-side.
  • If you are using SSG however, you will need to whitelist the external scripts either by name or by integrity hash.

For maximum compatibility between the two modes, it is easier to whitelist the external scripts by name:

export defaultNuxtConfig({
  security: {
    nonce: true,
    ssg: {
      hashScripts: true // In the SSG case, inline scripts generated by the server will be allowed by hash
    },
    headers: {
      contentSecurityPolicy: {
        "script-src": [
          "'nonce-{{nonce}}'", // Nonce placeholders in the SSR case will allow inline scripts generated on the server
          "'self'", // Allow external scripts from self origin
          "sha384-....." // Whitelist your external scripts by integrity hash in the SSG case
          "https://domain.com/external-script.js", // Or by fully-qualified name, this is compatible with both SSR and SSG
          "https://trusted-domain.com" // Or by domain if you can fully trust the domain, will also be compatible with both SSR and SSG
        ]
      }
    }
  }
})
If your Nuxt application is not inserting inline scripts on the client-side, you can still get a Level 2 Strict CSP without using 'strict-dynamic'.
You will need to be careful to whitelist all of your external scripts.

Not Strict CSP

If all of the above solutions have failed, you will need to go back to CSP Level 1 (not strict) with 'unsafe-inline'.

export defaultNuxtConfig({
  security: {
    nonce: false,
    ssg: {
      hashScripts: false // Disable hashes because they would cancel 'unsafe-inline'
    },
    headers: {
      contentSecurityPolicy: {
        "script-src": [
          "'unsafe-inline'", // Allow unsafe inline scripts: unsafe !
          //"'nonce-{{nonce}}'", // Disable nonce placeholders in the SSR case because they would cancel 'unsafe-inline'
          "https://domain.com/external-script.js", // Whitelist your external scripts by fully-qualified name
          "https://trusted-domain.com" // Or by domain if you can fully trust the domain
        ]
      }
    }
  }
})
Although valid from a CSP syntax perspective, allowing 'unsafe-inline' on script-src is unsafe.
This setup is not a Script CSP

Additional considerations

CSP Headers

There are two ways for the CSP policies to be transmitted from server to browser: either via the response's HTTP headers or via the document HTML's <meta http-equiv> tag.

It is generally considered safer to send CSP policies via HTTP headers, because in that case, the browser can check the policies before parsing the HTML. On the opposite, when the HTML <meta> tag is used, this tag will be read after the first elements of the <head> of the document.

Nuxt Security uses a different approach, depending on whether SSR or SSG is used.

  • In SSR mode we use the HTTP headers mechanism. This is because nuxi build spins off a Nitro server instance that can be used to modify the HTTP headers on the fly.
  • In SSG mode we use the HTML <meta> mechanism. This is because nuxi generate does not build a server. It only builds a collection of static HTML files that you then need to upload to a static server or a CDN. We have therefore have no control whatsoever over the HTTP headers that this external server will generate, and we need to resort to the fallback HTML approach.

CSP Headers for SSG via Nitro Presets

Nuxt Security supports CSP via HTTP headers for Nitro Presets that generate HTTP headers.

When using the SSG mode, some static hosting services such as Vercel or Netlify provide the ability to specify a configuration file that governs the value of the headers that will be generated. When these hosting services benefit from a Nitro Preset, it is possible for Nuxt Security to predict the value of the CSP headers for each page and write the value to the configuration file.

This feature is enabled by default with the ssg: exportToPresets option.

If you deploy your SSG site on Vercel or Netlify, you will benefit automatically from CSP Headers.
CSP will be delivered via HTTP headers, in addition to the standard <meta http-equiv> approach. If you want to disable the meta tag, so that only the HTTP headers are used, you can do so with the ssg: meta option.

CSP Headers for SSG via prerenderedHeaders hook

Nuxt Security allows you to generate your own headers rules with the nuxt-security:prerenderedHeaders buildtime hook.

If you do not deploy with a Nitro preset, or if you have specific requirements that are not met by the ssg: exportToPresets default, you can use this hook to generate your headers configuration file yourself.

See our documentation on the prerenderedPages hook

This will allow you to deliver CSP via HTTP headers, in addition to the standard <meta http-equiv> approach.

CSP Headers for Hybrid Pre-Rendered Pages

Nuxt Security supports CSP via HTTP headers for pre-rendered pages of Hybrid applications.

This feature is enabled by default with the ssg: nitroHeaders option.

In Hybrid applications, CSP of pre-rendered pages will be delivered via HTTP headers, in addition to the standard <meta http-equiv> approach.
If you want to disable the meta tag, so that only the HTTP headers are used, you can do so with the ssg: meta option.

Per Route CSP

Nuxt Security gives you the ability to define per-route CSP. For instance, you can have Strict CSP on the admin section of your application, and a more relaxed policy on the blog section.

However you should keep in mind that defining a per-route CSP can lead to issues:

  • When a user loads your Nuxt application for the first time from the /index page, the CSP headers returned by the server will be those of the index page.
  • When the user then navigates to the /admin section of your site, this navigation happens on the client-side. No additional request is being made to the server. Consequently, the CSP policy in force is still the one of the /index page. In order to refresh the policies, you need to force a hard-reload of the page, e.g. via reloadNuxtApp() or via <NuxtLink :external="true">.

These considerations are equally true for SSR (where the server needs to be hit again to recalculate the CSP headers), and for SSG (where the server needs to be hit again to serve the new static HTML file).

If you implement per-route CSP, you will need to enforce an external reload upon navigation for the new CSP to enter into action.
Please see our FAQ section on Updating Headers on a specific route
From an application design perspective, it is simpler to use a single Strict CSP that will apply to all of your application pages.

Conclusion

In order to obtain a Strict CSP on Nuxt apps, we need to use strict-dynamic. This mode disallows the ability for scripts to insert inline styles, and cancels the ability to whitelist external resources by name. In conjunction with the fact that nonces and hashes disable the 'unsafe-inline' mode, this leaves us with very few options to customize our CSP policies.

On the other hand, it obliges the developers community to adopt a standardized mindset when thinking about CSP. Less configuration options means less potential loopholes that malicious actors can seek to exploit.

With this in mind, we recommend that you implement your Strict CSP policy by checking your configuration against the following template:

export default defineNuxtConfig({
  security: {
    nonce: true, // Enables HTML nonce support in SSR mode
    ssg: {
      meta: true, // Enables CSP as a meta tag in SSG mode
      hashScripts: true, // Enables CSP hash support for scripts in SSG mode
      hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
      nitroHeaders: true // Allow Nitro to serve security headers for pre-rendered routes 
      exportToPresets: true // Export pre-rendered security headers to Nitro presets
    },
    // You can use nonce and ssg simultaneously
    // Nuxt Security will take care of choosing the adequate parameters when you build for either SSR or SSG
    headers: {
      contentSecurityPolicy: {
        'script-src': [
          "'self'",  // Fallback value, will be ignored by browsers level 3
          "https://domain.com/external-script.js", // Fallback value, will be ignored by browsers level 3
          "'unsafe-inline'", // Fallback value, will be ignored by browsers level 2 & 3
          "'strict-dynamic'", // Strict CSP via 'strict-dynamic', supported by browsers level 3
          "'nonce-{{nonce}}'" // Enables CSP nonce support for scripts in SSR mode, supported browsers level 2 & 3
        ],
        'style-src': [
          "'self'", // Enables loading of stylesheets hosted on self origin
          "https://domain.com/file.css", // Use fully-qualified filenames rather than the https: generic
          "https://trusted-domain.com", // Avoid using domain stubs unless you can fully trust them
          "'unsafe-inline'" // Recommended default for most Nuxt apps, but make sure 'img-src' is properly set up
          //"'nonce-{{nonce}}'" // Disables CSP nonce support, otherwise would cancel 'unsafe-inline'
          // You can re-enable if your application does not modify inline styles dynamically
        ],
        "img-src": [
          "'self'", // Enables loading of images hosted on self origin
          "https://domain.com/img.png", // Use fully-qualified filenames rather than the https: generic
          "https://trusted-domain.com", // Avoid using domain stubs unless you can fully trust them
          "blob:" // If you use Blob to construct images dynamically from javascript
          // Qualifying img-src properly mitigates strongly against 'unsafe-inline' in style-src
        ],
        'font-src': [
          "'self'",  // Enables loading of fonts hosted on self origin
          "https://domain.com/font.woff", // Use fully-qualified filenames rather than the https: generic
          "https://trusted-domain.com" // Avoid using domain stubs unless you can fully trust them
        ],
        "worker-src": [
          "'self'", // Enables loading service worker from self origin,
          "blob:" // If you use PWA, it is likely that the worker will be instantiated from Blob
        ],
        "connect-src": [
          "'self'", // Enables fetching from self origin
          "https://api.domain.com/service", // Use largest prefix possible on API routes
          "wss://api.domain.com/messages" // Add Websocket qualifiers if used
        ],
        "object-src": [
          "'none'"
        ],
        "base-uri": [
          "'none'"
        ]
        // Do not use default-src
      }
    }
  }
})