Nuxt.js: Issue with generating CSP policy meta tag for (static) SPAs

Created on 19 Oct 2019  Â·  19Comments  Â·  Source: nuxt/nuxt.js

Version

v2.10.1

Reproduction link

https://github.com/PedroD/nuxt-csp-test

Steps to reproduce

The link presented is of a sample project created with Nuxt using $ npx create-nuxt-app <project-name>.

I made no changes to the source code, except for the nuxt.config.js file, where I added the render:csp property, and for the package.json I changed nuxt from 2.0.0 to 2.10.1.

In order to reproduce the error:

  1. navigate to the project root folder in your terminal
  2. run npm run generate
  3. use serve to serve the dist folder (npm i -g serve, to install it) and then serve dist/ to serve it)
  4. open the page in the browser (usually served on http://localhost:5000/ by default)

What is expected ?

When you inspect the HTML, it is expected to see a meta tag in the <head> of the page containing the CSP policies defined, with hashes, and also all scripts and styles (incl. inline) should have a hash referenced in their tags.

What is actually happening?

No meta tag is created in <head>, and no hashes are being referenced in scripts and styles.

It is as if the render:csp property is not even set in nuxt.config.js.

Additional Info

Here is the contents of the nuxt.config.js for this sample project:

const ALLOWED_HOSTS = ['*.google-analytics.com', '*.doubleclick.net', '*.googleusercontent.com']

export default {
  mode: 'spa',

  render: {
    csp: {
      reportOnly: false,
      addMeta: true,
      hashAlgorithm: 'sha256',
      unsafeInlineCompatiblity: true,
      policies: {
        'default-src': ["'self'", 'https:', ...ALLOWED_HOSTS],
        'script-src': ["'self'", "'strict-dynamic'", 'https:'],
        'style-src': ["'self'", "'strict-dynamic'", 'https:'],
        'frame-src': [],
        'object-src': ["'none'"],
        'base-uri': ["'self"]
      /* "report-uri": [
        "https://sentry.io/api/<project>/security/?sentry_key=<key>",
      ], */
      }
    }
  },

  /*
  ** Headers of the page
  */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },
  /*
  ** Global CSS
  */
  css: [
    'element-ui/lib/theme-chalk/index.css'
  ],
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '@/plugins/element-ui'
  ],
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module'
  ],
  /*
  ** Nuxt.js modules
  */
  modules: [
    // Doc: https://axios.nuxtjs.org/usage
    '@nuxtjs/axios',
    '@nuxtjs/pwa'
  ],
  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {
  },
  /*
  ** Build configuration
  */
  build: {
    transpile: [/^element-ui/],
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    }
  }
}
This bug report is available on Nuxt community (#c9929)
bug-report pending

Most helpful comment

Any news on this topic ? It's still not working in generate mode...

All 19 comments

@manniL hello!

Could you confirm if this is a bug?

thanks!

As of my current understanding, Nuxt does not suuport CSP for the generate goal.

Supporting generating CSP meta tags is very useful in generate mode though. What was the expected use case when this feature was added into server mode?

According to @nuxt/vue-renderer/dist/vue-renderer.js, CSP logic is included only in the SSRRenderer class, but not in SPARenderer.

Any news on this topic ? It's still not working in generate mode...

I give this a small push, any news? We run into the same problem in our nuxtJs application

Even on SSR mode this still forces me to use unsafe CSP options for CSS and scripts...

Having issues with this too.
Trying to setup CSP with the new nuxt static generator.
I do get a head meta tag, but it only contains "script-src" and nothing else. My settings are very similar to yours.

Really looking forward to getting support for this issue!

Having issues with this too.
Trying to setup CSP with the new nuxt static generator.
I do get a head meta tag, but it only contains "script-src" and nothing else. My settings are very similar to yours.

Having the same issue on Nuxt 2.13.3 with the static target.

FYI, if you work around this issue like I have by setting the CSP header in your layout's head() method, you will still want to set the csp in nuxt.config.js, or you will end up generating unsafe eval statements because this fix will not work correctly.

This also doesn't work well for SSR, because Nuxt forces me to allow unsafe inlines in styles and scripts anyway.

Can someone please explain how this would work properly?

If the files in the dist folder are generated only once, that means the hashes are generated once as well, so an attacker visiting the site can simply take a note of it, and use the same hash on their payload.

Since it is a static build, the hashes won't change until the next build. Depending on the release cycle and/or support status this could mean very long period of time.

Wouldn't in such case using the CSP hashes only provide false sense of security?

In fact one could set up their backend serving the dist folder to run all files through some php or other preprocessors and replace all occurrence of the hashes with a new one on every request. Only in that case this would create added security.

For that still Nuxt should be able to build with at least one hash, that can be configured to be replaced, so I'm not against this feature, but I don't think it would be an out of the box standalone solution anyway.

What do you think?

@BenceSzalai A csp hash is a hash of the inline script that you want to secure.

If you change the content of the inline script it is no long valid. And the csp policy will stop the script from executing.

If you want to manually add a csp header for an inline script for a static site you can go to:
https://report-uri.com/home/hash

And hash your inline script. And then add the hash to the script-scr part of your csp.

Right now Nuxt does not do this for static generation.

I am starting to consider just writing a bash script in my CI to grep all the inline scripts. Hash them, and add them to my Firebase cdn headers...
Also, I am unsure if adding the headers to the meta tag is as secure as the http header of the cdn/site.

You are right, sorry, I was thinking about using nonce, but said hash only because the recommended nonce value is a “cryptographically secure random token”, which to I referred in myself as a “hash”, as an easy way to generate one is to hash some random input. I didn't know about the hash CSP mechanism, causing this accidental confusion.

Indeed when using CSP hash there is no problem with generating them statically, so that is even better, because it means Nuxt.js could in theory really do this, while still it would need to also either:

  • dump all those hashes into a file somewhere and the backend would be needed to include all of them in the CSP response HTTP headers or
  • Nuxt.js could add them to the rendered spa html head section, making the whole solution self-reliant.

Still better than what I thought of, as with CSP nonces one would require to preprocess the SPA static files for each request to inject a different nonce every time.

Meanwhile I took a look at Nuxt.js sources. The CSP hashes are generated in the SSRRenderer class's async render method around here.

The same should be somehow ported into the same method of SPARenderer class.

But it is more like a question than a claim, as my understanding is very limited (yet).

@BenceSzalai
I have been looking at the SSRRenderer class too, I have the impression that this feature shouldn't be to hard to implement.
Its there. Its just not working for static and spa builds.

My understanding of the internals of nuxt are limited too, while we wait for this feature to be implemented I started looking at how to generate the hashes myself in github actions.

I use firebase, so I have to just append all my hashes to the headers in my firebase.json.
But it should be possible to implement for other solutions too.

I will update with a temporary CI solution when I have it finished.

To implement this for SPA we need to assess all the scripts (and imo styles) that needs hashing.

By looking at my test project I've noticed these inline elements added to the generated sources:

SPARenderer also includes some styles and scripts from the head section in nuxt.config.js but I think those are always external, meaning they can be covered by 'self' or other domain/url specific CSP rules.

So to make this happen:

  • the hash need to be dynamically calculated for the script in line 157 of spa.js
  • the code that is responsible for basically this functionality need to be enabled for SPA mode
  • hashes should be generated and added to all styles and scripts under nuxt.js/packages/vue-app/template/views/

    • it could be done once, but that's fragile as changes to the templates would need changes to the SPA code in a different place, also it does not plays nice with a selectable hashing algorithm,

    • so the templates should be parsed for style and script tags to calculate their hashes

Now by that effort we could simply parse the whole final html output for style and script tags and calculate the hashes for all of them, regardless if they came from templates or line 157 of spa.js, or in fact any other source, which is also good for compatibility with future additions.

Afaics this approach would be different compared to SSRRenderer, as that one collects the inline scripts in an array, calculates the hashes on those and only adds them to the HTML after, which is nice as it can avoid parsing HTML to find script tags, however I couldn't spot anything being included from nuxt.js/packages/vue-app/template/views/ for SSR, which allows this no-parsing approach. The same could not work for SPA using the template views as they are right now.

On the other hand I think SSRRenderer only supports hashing for the scripts, and not the styles. With the HTML parsing aproachif we do for scripts, its easy to do for styles as well.

Do you think it is a good and viable approach to parse the generated html for style and script tags?


Edit: node-html-parser is already required by Nuxt.js which provides suitable tool to do the parsing.

└─┬ [email protected]
  └─┬ @nuxt/[email protected]
    └── [email protected] 

In order to not deal with CSP for CSS I extract it on build:
nuxt.config.js

extractCSS: true,
optimization: {
    splitChunks: {
        cacheGroups: {
            styles: {
                name: "styles",
                test: /\.(css|vue)$/,
                chunks: "all",
                enforce: true,
            },
        },
    },
},

+1 on everything you said @BenceSzalai . This is what needs to be done.

If you need CSP right now I made some stupid bash commands to fix CSP for me with Github Actions and firebase.

      - name: Set Inline script CSP
        run: |
          find ./dist -name '*.html' | xargs sed -n 's:.*<script>\(.*\)</script>.*:\1:p' | sed 's/<\/script>.*//g' > nuxt-unsafe-inline-scripts
          cat nuxt-unsafe-inline-scripts | while read line; do printf $line | openssl sha256 -binary | openssl base64 >> csp-hashes; done
          cat csp-hashes | while read line; do  echo "'sha256-$line'" >> sha256-csp-hashes; done
          sed -i -r "s:(script-src):\1 $(paste -sd ' ' sha256-csp-hashes) :g" firebase.json

Having my CSP defined at the CDN and not nuxt is fine with me for the time beeing.

The script gets all the inline inline loading chunks, creates their hashes and saves them to my firebase.json
It won't work with other inline scripts that are more than one line of code and it wont work if there are more than one inline script in a generated html file.
Its a temporary solution for me.

The script has a prerequisite that you have a firebase.json with atleast the script-src value:

{
   "hosting":{
      "public":"dist",
      "ignore":[
         "firebase.json",
         "**/.*",
         "**/node_modules/**"
      ],
      "headers":[
         {
            "source":"**",
            "headers":[
               {
                  "key":"Content-Security-Policy",
                  "value":"default-src 'self' ; script-src 'self'"
               }
            ]
         }
      ]
   }
}

Turns out this cannot be achieved inside SPARenderer, because the html is returned in generator.js#L209 and immediately modified by HTML minifier here generator.js#L215. So CSP hashes can only be calculated after the minification.

Anyway, I've made this in generator. See my solution here: https://github.com/BenceSzalai/nuxt.js/commit/6aba46f54e1df63a68ceaebf1e44c7f627f29745 and let me know if it worth a pull request.

In fact the getCspString and transformPolicyObject functions are almost the same as in packages/server/src/middleware/nuxt.js, but are extended to support style tags on top of scripts. I think it would be best to factor the two together, but I'm not sure where those two should be.

If anyone wants to try it with monkey patching replace your node_modules/@nuxt/generator/dist/generator.js with this: https://gist.github.com/BenceSzalai/5e6db79035863d6a0b7f7328c7072ae3

It supports 2 new on top of the existing options:

  • showResult: boolean - Displays the generated CSP string in the console, useful for one time configuration after build
  • saveResult: false or filename string - Saves the CSP HTTP Header in this file in the output folder (/dist by default). This is usefull if the backend can be configured to automatically attach the contents of the file to the HTTP response as an additional header.

Also this existing option may behave differently compared to SSR:

  • addMeta: boolean - Adds the meta tag to HTML (note that report-uri only allowed in HTTP header, so automatically excluded from the meta tag, even if added to the policy)

See example configuration here: https://gist.github.com/BenceSzalai/5e6db79035863d6a0b7f7328c7072ae3#file-nuxt-config-js

Let me know if this is good enoug for a PR!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

o-alexandrov picture o-alexandrov  Â·  3Comments

jaredreich picture jaredreich  Â·  3Comments

uptownhr picture uptownhr  Â·  3Comments

lazycrazy picture lazycrazy  Â·  3Comments

mikekidder picture mikekidder  Â·  3Comments