mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +00:00
fix: csp SHA hashes accumulate when using custom script-src rules (#4519)
[skip ci]
This commit is contained in:
parent
13c7b6d671
commit
683dbba4f7
@ -15,7 +15,7 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
|
||||
await nuxt.callHook('render:route', url, result, context)
|
||||
const {
|
||||
html,
|
||||
cspScriptSrcHashSet,
|
||||
cspScriptSrcHashes,
|
||||
error,
|
||||
redirected,
|
||||
getPreloadFiles
|
||||
@ -66,7 +66,7 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
|
||||
const { allowedSources, policies } = options.render.csp
|
||||
const cspHeader = options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
|
||||
|
||||
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashSet, allowedSources, policies, isDev: options.dev }))
|
||||
res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isDev: options.dev }))
|
||||
}
|
||||
|
||||
// Send response
|
||||
@ -114,9 +114,9 @@ const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => {
|
||||
return links
|
||||
}
|
||||
|
||||
const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev }) => {
|
||||
const joinedHashSet = Array.from(cspScriptSrcHashSet).join(' ')
|
||||
const baseCspStr = `script-src 'self'${isDev ? ` 'unsafe-eval'` : ''} ${joinedHashSet}`
|
||||
const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev }) => {
|
||||
const joinedHashes = cspScriptSrcHashes.join(' ')
|
||||
const baseCspStr = `script-src 'self'${isDev ? ` 'unsafe-eval'` : ''} ${joinedHashes}`
|
||||
|
||||
if (Array.isArray(allowedSources)) {
|
||||
return `${baseCspStr} ${allowedSources.join(' ')}`
|
||||
@ -125,7 +125,7 @@ const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev })
|
||||
const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies)
|
||||
|
||||
if (policyObjectAvailable) {
|
||||
const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashSet)
|
||||
const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes)
|
||||
|
||||
return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${v.join(' ')}`).join('; ')
|
||||
}
|
||||
@ -133,22 +133,13 @@ const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev })
|
||||
return baseCspStr
|
||||
}
|
||||
|
||||
const transformPolicyObject = (policies, cspScriptSrcHashSet) => {
|
||||
const transformPolicyObject = (policies, cspScriptSrcHashes) => {
|
||||
const userHasDefinedScriptSrc = policies['script-src'] && Array.isArray(policies['script-src'])
|
||||
|
||||
const additionalPolicies = userHasDefinedScriptSrc ? policies['script-src'] : []
|
||||
|
||||
// Self is always needed for inline-scripts, so add it, no matter if the user specified script-src himself.
|
||||
const hashAndPolicyList = cspScriptSrcHashes.concat(`'self'`, additionalPolicies)
|
||||
|
||||
const hashAndPolicySet = cspScriptSrcHashSet
|
||||
hashAndPolicySet.add(`'self'`)
|
||||
|
||||
if (!userHasDefinedScriptSrc) {
|
||||
policies['script-src'] = Array.from(hashAndPolicySet)
|
||||
return policies
|
||||
}
|
||||
|
||||
new Set(policies['script-src']).forEach(src => hashAndPolicySet.add(src))
|
||||
|
||||
policies['script-src'] = Array.from(hashAndPolicySet)
|
||||
|
||||
return policies
|
||||
return { ...policies, 'script-src': hashAndPolicyList }
|
||||
}
|
||||
|
@ -365,12 +365,12 @@ export default class VueRenderer {
|
||||
|
||||
const serializedSession = `window.${this.context.globals.context}=${devalue(context.nuxt)};`
|
||||
|
||||
const cspScriptSrcHashSet = new Set()
|
||||
const cspScriptSrcHashes = []
|
||||
if (this.context.options.render.csp) {
|
||||
const { hashAlgorithm } = this.context.options.render.csp
|
||||
const hash = crypto.createHash(hashAlgorithm)
|
||||
hash.update(serializedSession)
|
||||
cspScriptSrcHashSet.add(`'${hashAlgorithm}-${hash.digest('base64')}'`)
|
||||
cspScriptSrcHashes.push(`'${hashAlgorithm}-${hash.digest('base64')}'`)
|
||||
}
|
||||
|
||||
APP += `<script>${serializedSession}</script>`
|
||||
@ -390,7 +390,7 @@ export default class VueRenderer {
|
||||
|
||||
return {
|
||||
html,
|
||||
cspScriptSrcHashSet,
|
||||
cspScriptSrcHashes,
|
||||
getPreloadFiles: this.getPreloadFiles.bind(this, context),
|
||||
error: context.nuxt.error,
|
||||
redirected: context.redirected
|
||||
|
@ -312,5 +312,30 @@ describe('basic ssr csp', () => {
|
||||
expect(uniqueHashes.length).toBe(hashes.length)
|
||||
}
|
||||
)
|
||||
test(
|
||||
'Not contain old hashes when loading new page',
|
||||
async () => {
|
||||
const cspOption = {
|
||||
enabled: true,
|
||||
policies: {
|
||||
'default-src': [`'self'`],
|
||||
'script-src': ['https://example.com', 'https://example.io']
|
||||
}
|
||||
}
|
||||
nuxt = await startCspDevServer(cspOption)
|
||||
const { headers: user1Header } = await rp(url('/users/1'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
const user1Hashes = user1Header[reportOnlyHeader].split(' ').filter(s => s.startsWith('\'sha256-'))
|
||||
|
||||
const { headers: user2Header } = await rp(url('/users/2'), {
|
||||
resolveWithFullResponse: true
|
||||
})
|
||||
const user2Hashes = new Set(user2Header[reportOnlyHeader].split(' ').filter(s => s.startsWith('\'sha256-')))
|
||||
|
||||
const intersection = new Set(user1Hashes.filter(x => user2Hashes.has(x)))
|
||||
expect(intersection.size).toBe(0)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user