fix: csp SHA hashes accumulate when using custom script-src rules (#4519)

[skip ci]
This commit is contained in:
William Chong 2018-12-12 14:29:28 +08:00 committed by Pooya Parsa
parent 13c7b6d671
commit 683dbba4f7
3 changed files with 39 additions and 23 deletions

View File

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

View File

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

View File

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