mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-17 06:01:34 +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)
|
await nuxt.callHook('render:route', url, result, context)
|
||||||
const {
|
const {
|
||||||
html,
|
html,
|
||||||
cspScriptSrcHashSet,
|
cspScriptSrcHashes,
|
||||||
error,
|
error,
|
||||||
redirected,
|
redirected,
|
||||||
getPreloadFiles
|
getPreloadFiles
|
||||||
@ -66,7 +66,7 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux
|
|||||||
const { allowedSources, policies } = options.render.csp
|
const { allowedSources, policies } = options.render.csp
|
||||||
const cspHeader = options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
|
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
|
// Send response
|
||||||
@ -114,9 +114,9 @@ const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => {
|
|||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev }) => {
|
const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev }) => {
|
||||||
const joinedHashSet = Array.from(cspScriptSrcHashSet).join(' ')
|
const joinedHashes = cspScriptSrcHashes.join(' ')
|
||||||
const baseCspStr = `script-src 'self'${isDev ? ` 'unsafe-eval'` : ''} ${joinedHashSet}`
|
const baseCspStr = `script-src 'self'${isDev ? ` 'unsafe-eval'` : ''} ${joinedHashes}`
|
||||||
|
|
||||||
if (Array.isArray(allowedSources)) {
|
if (Array.isArray(allowedSources)) {
|
||||||
return `${baseCspStr} ${allowedSources.join(' ')}`
|
return `${baseCspStr} ${allowedSources.join(' ')}`
|
||||||
@ -125,7 +125,7 @@ const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev })
|
|||||||
const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies)
|
const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies)
|
||||||
|
|
||||||
if (policyObjectAvailable) {
|
if (policyObjectAvailable) {
|
||||||
const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashSet)
|
const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes)
|
||||||
|
|
||||||
return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${v.join(' ')}`).join('; ')
|
return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${v.join(' ')}`).join('; ')
|
||||||
}
|
}
|
||||||
@ -133,22 +133,13 @@ const getCspString = ({ cspScriptSrcHashSet, allowedSources, policies, isDev })
|
|||||||
return baseCspStr
|
return baseCspStr
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformPolicyObject = (policies, cspScriptSrcHashSet) => {
|
const transformPolicyObject = (policies, cspScriptSrcHashes) => {
|
||||||
const userHasDefinedScriptSrc = policies['script-src'] && Array.isArray(policies['script-src'])
|
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.
|
// 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
|
return { ...policies, 'script-src': hashAndPolicyList }
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -365,12 +365,12 @@ export default class VueRenderer {
|
|||||||
|
|
||||||
const serializedSession = `window.${this.context.globals.context}=${devalue(context.nuxt)};`
|
const serializedSession = `window.${this.context.globals.context}=${devalue(context.nuxt)};`
|
||||||
|
|
||||||
const cspScriptSrcHashSet = new Set()
|
const cspScriptSrcHashes = []
|
||||||
if (this.context.options.render.csp) {
|
if (this.context.options.render.csp) {
|
||||||
const { hashAlgorithm } = this.context.options.render.csp
|
const { hashAlgorithm } = this.context.options.render.csp
|
||||||
const hash = crypto.createHash(hashAlgorithm)
|
const hash = crypto.createHash(hashAlgorithm)
|
||||||
hash.update(serializedSession)
|
hash.update(serializedSession)
|
||||||
cspScriptSrcHashSet.add(`'${hashAlgorithm}-${hash.digest('base64')}'`)
|
cspScriptSrcHashes.push(`'${hashAlgorithm}-${hash.digest('base64')}'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
APP += `<script>${serializedSession}</script>`
|
APP += `<script>${serializedSession}</script>`
|
||||||
@ -390,7 +390,7 @@ export default class VueRenderer {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
html,
|
html,
|
||||||
cspScriptSrcHashSet,
|
cspScriptSrcHashes,
|
||||||
getPreloadFiles: this.getPreloadFiles.bind(this, context),
|
getPreloadFiles: this.getPreloadFiles.bind(this, context),
|
||||||
error: context.nuxt.error,
|
error: context.nuxt.error,
|
||||||
redirected: context.redirected
|
redirected: context.redirected
|
||||||
|
@ -312,5 +312,30 @@ describe('basic ssr csp', () => {
|
|||||||
expect(uniqueHashes.length).toBe(hashes.length)
|
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