diff --git a/packages/config/src/options.js b/packages/config/src/options.js index ff90acde9c91..0e5365d343a7 100644 --- a/packages/config/src/options.js +++ b/packages/config/src/options.js @@ -280,7 +280,8 @@ export function getNuxtConfig (_options) { policies: undefined, addMeta: Boolean(options.target === TARGETS.static), unsafeInlineCompatibility: false, - reportOnly: options.debug + reportOnly: options.debug, + generateNonce: false }) // TODO: Remove this if statement in Nuxt 3, we will stop supporting this typo (more on: https://github.com/nuxt/nuxt.js/pull/6583) diff --git a/packages/config/test/options.test.js b/packages/config/test/options.test.js index 365d1ddfd046..ea577782ff0b 100644 --- a/packages/config/test/options.test.js +++ b/packages/config/test/options.test.js @@ -116,7 +116,8 @@ describe('config: options', () => { allowedSources: ['/nuxt/*'], policies: undefined, reportOnly: false, - test: true + test: true, + generateNonce: false }) }) @@ -130,7 +131,8 @@ describe('config: options', () => { allowedSources: ['/nuxt/*'], policies: undefined, reportOnly: false, - test: true + test: true, + generateNonce: false }) }) diff --git a/packages/types/config/render.d.ts b/packages/types/config/render.d.ts index 65f5a55c2b67..c89bee77256d 100644 --- a/packages/types/config/render.d.ts +++ b/packages/types/config/render.d.ts @@ -34,6 +34,7 @@ interface CspOptions { addMeta?: boolean allowedSources?: string[] hashAlgorithm?: string + generateNonce?: boolean policies?: Partial> reportOnly?: boolean unsafeInlineCompatibility?: boolean diff --git a/packages/vue-renderer/src/renderers/ssr.js b/packages/vue-renderer/src/renderers/ssr.js index e58b169ebc2f..67b5ea82c7ed 100644 --- a/packages/vue-renderer/src/renderers/ssr.js +++ b/packages/vue-renderer/src/renderers/ssr.js @@ -21,7 +21,7 @@ export default class SSRRenderer extends BaseRenderer { } } - addAttrs (tags, referenceTag, referenceAttr) { + addAttrs (renderContext, tags, referenceTag, referenceAttr) { const reference = referenceTag ? `<${referenceTag}` : referenceAttr if (!reference) { return tags @@ -35,15 +35,23 @@ export default class SSRRenderer extends BaseRenderer { ) } + const { req } = renderContext + if (req && typeof req.__nonce_value__ === 'string') { + tags = tags.replace( + new RegExp(reference, 'g'), + `${reference} nonce="${req.__nonce_value__}"` + ) + } + return tags } renderResourceHints (renderContext) { - return this.addAttrs(renderContext.renderResourceHints(), null, 'rel="preload"') + return this.addAttrs(renderContext, renderContext.renderResourceHints(), null, 'rel="preload"') } renderScripts (renderContext) { - let renderedScripts = this.addAttrs(renderContext.renderScripts(), 'script') + let renderedScripts = this.addAttrs(renderContext, renderContext.renderScripts(), 'script') if (this.options.render.asyncScripts) { renderedScripts = renderedScripts.replace(/defer>/g, 'defer async>') } @@ -51,7 +59,7 @@ export default class SSRRenderer extends BaseRenderer { } renderStyles (renderContext) { - return this.addAttrs(renderContext.renderStyles(), 'link') + return this.addAttrs(renderContext, renderContext.renderStyles(), 'link') } getPreloadFiles (renderContext) { @@ -152,6 +160,12 @@ export default class SSRRenderer extends BaseRenderer { meta.noscript.text() } + const { csp } = this.options.render + const { req = {} } = renderContext + if (csp && csp.generateNonce === true) { + req.__nonce_value__ = crypto.randomBytes(32).toString('hex') + } + // Check if we need to inject scripts and state const shouldInjectScripts = this.options.render.injectScripts !== false @@ -178,7 +192,6 @@ export default class SSRRenderer extends BaseRenderer { } } - const { csp } = this.options.render // Only add the hash if 'unsafe-inline' rule isn't present to avoid conflicts (#5387) const containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes('\'unsafe-inline\'') const shouldHashCspScriptSrc = csp && (csp.unsafeInlineCompatibility || !containsUnsafeInlineScriptSrc) @@ -257,6 +270,10 @@ export default class SSRRenderer extends BaseRenderer { } } + if (req.__nonce_value__) { + cspScriptSrcHashes.push(`'nonce-${req.__nonce_value__}'`) + } + // Call ssr:csp hook await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes) diff --git a/test/dev/basic.ssr.csp.test.js b/test/dev/basic.ssr.csp.test.js index f40186c85950..63189b48a77b 100644 --- a/test/dev/basic.ssr.csp.test.js +++ b/test/dev/basic.ssr.csp.test.js @@ -217,6 +217,26 @@ describe('basic ssr csp', () => { } ) + test('Contain nonce on ssr links and scripts', async () => { + nuxt = await startCspServer({ + generateNonce: true + }) + + const { body, headers } = await rp(url('/stateless')) + + expect(headers[cspHeader]).toMatch(/script-src .* 'nonce-.*'/) + + const nonceValue = headers[cspHeader].match(/'nonce-(.*?)'/)[1] + + for (const link of body.match(/]+?>/g)) { + expect(link).toContain(`nonce="${nonceValue}"`) + } + + for (const script of body.match(/]+?>/g)) { + expect(script).toContain(`nonce="${nonceValue}"`) + } + }) + // TODO: Remove this test in Nuxt 3, we will stop supporting this typo (more on: https://github.com/nuxt/nuxt.js/pull/6583) test( 'Contain hash and \'unsafe-inline\' when the typo property unsafeInlineCompatiblity is enabled',