mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-26 23:52:06 +00:00
feat(csp): support generating nonce for scripts and links in ssr (#9621)
This commit is contained in:
parent
7cd2b19b44
commit
89204f057b
@ -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)
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
|
1
packages/types/config/render.d.ts
vendored
1
packages/types/config/render.d.ts
vendored
@ -34,6 +34,7 @@ interface CspOptions {
|
||||
addMeta?: boolean
|
||||
allowedSources?: string[]
|
||||
hashAlgorithm?: string
|
||||
generateNonce?: boolean
|
||||
policies?: Partial<Record<CspPolicyName, string[]>>
|
||||
reportOnly?: boolean
|
||||
unsafeInlineCompatibility?: boolean
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user