feat(csp): support generating nonce for scripts and links in ssr (#9621)

This commit is contained in:
Xin Du (Clark) 2023-06-07 20:10:41 +01:00 committed by GitHub
parent 7cd2b19b44
commit 89204f057b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 8 deletions

View File

@ -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)

View File

@ -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
})
})

View File

@ -34,6 +34,7 @@ interface CspOptions {
addMeta?: boolean
allowedSources?: string[]
hashAlgorithm?: string
generateNonce?: boolean
policies?: Partial<Record<CspPolicyName, string[]>>
reportOnly?: boolean
unsafeInlineCompatibility?: boolean

View File

@ -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)

View File

@ -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(/<link[^>]+?>/g)) {
expect(link).toContain(`nonce="${nonceValue}"`)
}
for (const script of body.match(/<script[^>]+?>/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',