Nuxt.js: generate csp nonce on every request

Created on 19 Dec 2019  路  11Comments  路  Source: nuxt/nuxt.js

What problem does this feature solve?

In a CSP environment, there are cases where you need to use CSP nonce to load your scripts.
One example in the vue ecosystem is vuetify.
At the moment you can鈥檛 use nuxt and vuetify with CSP enabled without a static nonce (which is insecure).

What does the proposed changes look like?

When CSP is enabled and we are in universal mode, a 128 bit random nonce should be generated on every request. This nonce should be added to to the CSP header and should be consumable from plugins and pages for script loading.

feature-request

Most helpful comment

@armorkram I just wrote a module which achieves that. Note that it is not well-tested at the moment. If you find an issue with it, please notify me about it.

import { randomBytes } from 'crypto'

export default function cspModule() {
  // Set nuxt CSP options.
  this.options.render.csp = {
    reportOnly: true,
    hashAlgorithm: 'sha256',
  }

  this.nuxt.hook('render:routeContext', (nuxtContext) => {
    // Generate a 128 bit random nonce every request.
    const nonce = randomBytes(16).toString('base64')
    // Inject nonce into vuex state before state is serialized into window.__NUXT__.
    nuxtContext.state.nonce = nonce
  })

  this.nuxt.hook(
    'render:route',
    (url, { cspScriptSrcHashes }, { nuxt: nuxtContext }) => {
      // Extract nonce generated in render:routeContext.
      const nonce = nuxtContext.state.nonce
      // Add nonce to cspScriptSrcHashes. Nuxt will populate all entries in this array
      // to the csp header and meta tags as part of the script-src csp policy.
      cspScriptSrcHashes.push(`'nonce-${nonce}'`)
    }
  )
}

One caveat of this method is that the nonce is only for the script-src csp policy. But in most cases it is only needed for scripts anyways.

All 11 comments

already implemented but disabled by default, you have to enable it in nuxt.config.js

render: {
csp: true
}

@talebi1 This does enable csp and adds the hashes off all sources, but it does not generate a csp nonce every request. The idea is to have that nonce where the sources approach does not work, e.g. for vuetify or when loading google analytics or PayPal.

@P4sca1 have you tried using a serverMiddleware to generate the nonce?

for example:

(req, res, next) => {
  // Set nonce
  const nonce = crypto.randomBytes(16).toString('base64');
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'nonce-${nonce}' 'strict-dynamic' https:`
  )

@talebi1 I also thought about this, but faced 2 problems:
1) How to merge my csp header with the header generated by nuxt
2) How to pass the generated nonce into nuxt to use it in plugins and components

@talebi1 I also thought about this, but faced 2 problems:

  1. How to merge my csp header with the header generated by nuxt
  2. How to pass the generated nonce into nuxt to use it in plugins and components

The normal behaviour imho should be disable the CSP from Server and use the one from Nuxt.

On apache inside of vhost you should set:

Header unset Content-Security-Policy

And set all CSP settings on nuxt.config.js from the link:

https://nuxtjs.org/api/configuration-render/#csp

The problem is:

Seems is bugged, working only the mode:

reportOnly: true,

If set to false, at least on Apache, CSP Header is not set.

I will open an issue about it, just looking before if there similar issue.

By merging my csp headers I meant merging it inside my application and not in a web server like Apache.
Nuxt generates csp hashes and I want to merge these hashes with my nonce.

If you don't disable the CSP from web server (Apache / Nginx / etc...) that will be a mess.

This is why I have pointed about disable CSP from web server first.

Nuxt will generate the hashes, and if you need an global constant for use as nonce, you can check how to add it on nuxt.config.js on the link:

https://stackoverflow.com/questions/50069628/how-to-generate-a-nonce-in-node-js

Was anyone able to get this to work? I'm trying this as well but I couldn't find a way to add/merge the CSP header. I have CSP configured in Nuxt's render config and it's overriding the headers I set in the serverMiddleware. Here's my setup.

In serverMiddleware:

  1. Generate nonce
  2. Set Content-Security-Header header
  3. Attach nonce to the request object

In store:

  1. In nuxtServerInit, get nonce from request object and store in state

In components/plugins:

  1. Attach nonce to inline scripts

Is there a way to merge the header from the serverMiddleware with the CSP generated by Nuxt? The only work-around I can think of is moving all CSP headers to the serverMiddleware.

@armorkram I just wrote a module which achieves that. Note that it is not well-tested at the moment. If you find an issue with it, please notify me about it.

import { randomBytes } from 'crypto'

export default function cspModule() {
  // Set nuxt CSP options.
  this.options.render.csp = {
    reportOnly: true,
    hashAlgorithm: 'sha256',
  }

  this.nuxt.hook('render:routeContext', (nuxtContext) => {
    // Generate a 128 bit random nonce every request.
    const nonce = randomBytes(16).toString('base64')
    // Inject nonce into vuex state before state is serialized into window.__NUXT__.
    nuxtContext.state.nonce = nonce
  })

  this.nuxt.hook(
    'render:route',
    (url, { cspScriptSrcHashes }, { nuxt: nuxtContext }) => {
      // Extract nonce generated in render:routeContext.
      const nonce = nuxtContext.state.nonce
      // Add nonce to cspScriptSrcHashes. Nuxt will populate all entries in this array
      // to the csp header and meta tags as part of the script-src csp policy.
      cspScriptSrcHashes.push(`'nonce-${nonce}'`)
    }
  )
}

One caveat of this method is that the nonce is only for the script-src csp policy. But in most cases it is only needed for scripts anyways.

@P4sca1 Thank you for this! It works quite well. Having it work only for script-src shouldn't be an issue for most cases, as you've mentioned.

The only thing that needs to be pointed out is that setting that Nuxt CSP options here will override the CSP render options in nuxt.config, since a new object is assigned to this.options.render.csp. Other policies will essentially be removed. Merging the CSP options or individually setting the properties would be necessary to avoid this.

Yeah that's absolutely correct. I do not set the options in nuxt.config.js and prefer to have them in the module so that my nuxt.config.js gets a little bit more readable.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vadimsg picture vadimsg  路  3Comments

pehbehbeh picture pehbehbeh  路  3Comments

gary149 picture gary149  路  3Comments

vadimsg picture vadimsg  路  3Comments

uptownhr picture uptownhr  路  3Comments