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,
|
policies: undefined,
|
||||||
addMeta: Boolean(options.target === TARGETS.static),
|
addMeta: Boolean(options.target === TARGETS.static),
|
||||||
unsafeInlineCompatibility: false,
|
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)
|
// 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/*'],
|
allowedSources: ['/nuxt/*'],
|
||||||
policies: undefined,
|
policies: undefined,
|
||||||
reportOnly: false,
|
reportOnly: false,
|
||||||
test: true
|
test: true,
|
||||||
|
generateNonce: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -130,7 +131,8 @@ describe('config: options', () => {
|
|||||||
allowedSources: ['/nuxt/*'],
|
allowedSources: ['/nuxt/*'],
|
||||||
policies: undefined,
|
policies: undefined,
|
||||||
reportOnly: false,
|
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
|
addMeta?: boolean
|
||||||
allowedSources?: string[]
|
allowedSources?: string[]
|
||||||
hashAlgorithm?: string
|
hashAlgorithm?: string
|
||||||
|
generateNonce?: boolean
|
||||||
policies?: Partial<Record<CspPolicyName, string[]>>
|
policies?: Partial<Record<CspPolicyName, string[]>>
|
||||||
reportOnly?: boolean
|
reportOnly?: boolean
|
||||||
unsafeInlineCompatibility?: 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
|
const reference = referenceTag ? `<${referenceTag}` : referenceAttr
|
||||||
if (!reference) {
|
if (!reference) {
|
||||||
return tags
|
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
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
renderResourceHints (renderContext) {
|
renderResourceHints (renderContext) {
|
||||||
return this.addAttrs(renderContext.renderResourceHints(), null, 'rel="preload"')
|
return this.addAttrs(renderContext, renderContext.renderResourceHints(), null, 'rel="preload"')
|
||||||
}
|
}
|
||||||
|
|
||||||
renderScripts (renderContext) {
|
renderScripts (renderContext) {
|
||||||
let renderedScripts = this.addAttrs(renderContext.renderScripts(), 'script')
|
let renderedScripts = this.addAttrs(renderContext, renderContext.renderScripts(), 'script')
|
||||||
if (this.options.render.asyncScripts) {
|
if (this.options.render.asyncScripts) {
|
||||||
renderedScripts = renderedScripts.replace(/defer>/g, 'defer async>')
|
renderedScripts = renderedScripts.replace(/defer>/g, 'defer async>')
|
||||||
}
|
}
|
||||||
@ -51,7 +59,7 @@ export default class SSRRenderer extends BaseRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStyles (renderContext) {
|
renderStyles (renderContext) {
|
||||||
return this.addAttrs(renderContext.renderStyles(), 'link')
|
return this.addAttrs(renderContext, renderContext.renderStyles(), 'link')
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreloadFiles (renderContext) {
|
getPreloadFiles (renderContext) {
|
||||||
@ -152,6 +160,12 @@ export default class SSRRenderer extends BaseRenderer {
|
|||||||
meta.noscript.text()
|
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
|
// Check if we need to inject scripts and state
|
||||||
const shouldInjectScripts = this.options.render.injectScripts !== false
|
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)
|
// 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 containsUnsafeInlineScriptSrc = csp.policies && csp.policies['script-src'] && csp.policies['script-src'].includes('\'unsafe-inline\'')
|
||||||
const shouldHashCspScriptSrc = csp && (csp.unsafeInlineCompatibility || !containsUnsafeInlineScriptSrc)
|
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
|
// Call ssr:csp hook
|
||||||
await this.serverContext.nuxt.callHook('vue-renderer:ssr:csp', cspScriptSrcHashes)
|
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)
|
// TODO: Remove this test in Nuxt 3, we will stop supporting this typo (more on: https://github.com/nuxt/nuxt.js/pull/6583)
|
||||||
test(
|
test(
|
||||||
'Contain hash and \'unsafe-inline\' when the typo property unsafeInlineCompatiblity is enabled',
|
'Contain hash and \'unsafe-inline\' when the typo property unsafeInlineCompatiblity is enabled',
|
||||||
|
Loading…
Reference in New Issue
Block a user