From 37271f8ac41447f33777bbf2720edd3d5d817523 Mon Sep 17 00:00:00 2001 From: Yugo Ogura Date: Tue, 5 May 2020 03:24:17 +0900 Subject: [PATCH] feat(server): support csp `report-uri` (#7307) --- packages/server/src/middleware/nuxt.js | 15 +++++----- test/dev/basic.ssr.csp.test.js | 40 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/server/src/middleware/nuxt.js b/packages/server/src/middleware/nuxt.js index ed9058be07..4eafffb450 100644 --- a/packages/server/src/middleware/nuxt.js +++ b/packages/server/src/middleware/nuxt.js @@ -70,9 +70,10 @@ export default ({ options, nuxt, renderRoute, resources }) => async function nux if (options.render.csp && cspScriptSrcHashes) { const { allowedSources, policies } = options.render.csp - const cspHeader = options.render.csp.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' + const isReportOnly = !!options.render.csp.reportOnly + const cspHeader = isReportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' - res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isDev: options.dev })) + res.setHeader(cspHeader, getCspString({ cspScriptSrcHashes, allowedSources, policies, isDev: options.dev, isReportOnly })) } // Send response @@ -123,20 +124,18 @@ const defaultPushAssets = (preloadFiles, shouldPush, publicPath, options) => { return links } -const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev }) => { +const getCspString = ({ cspScriptSrcHashes, allowedSources, policies, isDev, isReportOnly }) => { const joinedHashes = cspScriptSrcHashes.join(' ') const baseCspStr = `script-src 'self'${isDev ? ' \'unsafe-eval\'' : ''} ${joinedHashes}` + const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies) if (Array.isArray(allowedSources) && allowedSources.length) { - return `${baseCspStr} ${allowedSources.join(' ')}` + return isReportOnly && policyObjectAvailable && !!policies['report-uri'] ? `${baseCspStr} ${allowedSources.join(' ')}; report-uri ${policies['report-uri']};` : `${baseCspStr} ${allowedSources.join(' ')}` } - const policyObjectAvailable = typeof policies === 'object' && policies !== null && !Array.isArray(policies) - if (policyObjectAvailable) { const transformedPolicyObject = transformPolicyObject(policies, cspScriptSrcHashes) - - return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${v.join(' ')}`).join('; ') + return Object.entries(transformedPolicyObject).map(([k, v]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`).join('; ') } return baseCspStr diff --git a/test/dev/basic.ssr.csp.test.js b/test/dev/basic.ssr.csp.test.js index 205cd2ab3c..f40186c859 100644 --- a/test/dev/basic.ssr.csp.test.js +++ b/test/dev/basic.ssr.csp.test.js @@ -128,6 +128,26 @@ describe('basic ssr csp', () => { } ) + test( + 'Contain report-uri in Content-Security-Policy-Report-Only header, when explicitly asked for CSRP, allowedSources, csp.report-url', + async () => { + const cspOption = { + allowedSources: ['https://example.com', 'https://example.io'], + reportOnly: true, + policies: { + 'report-uri': '/csp_report_uri' + } + } + nuxt = await startCspDevServer(cspOption) + const { headers } = await rp(url('/stateless')) + + expect(headers[reportOnlyHeader]).toMatch(/^script-src 'self' 'sha256-.*'/) + expect(headers[reportOnlyHeader]).toContain('https://example.com') + expect(headers[reportOnlyHeader]).toContain('https://example.io') + expect(headers[reportOnlyHeader]).toContain('report-uri /csp_report_uri') + } + ) + test( 'Contain only unique hashes in header when csp.policies is set', async () => { @@ -302,6 +322,26 @@ describe('basic ssr csp', () => { } ) + test( + 'Contain report-uri in Content-Security-Policy-Report-Only header, when explicitly asked for CSRP, allowedSources, csp.report-url', + async () => { + const cspOption = { + allowedSources: ['https://example.com', 'https://example.io'], + reportOnly: true, + policies: { + 'report-uri': '/csp_report_uri' + } + } + nuxt = await startCspDevServer(cspOption) + const { headers } = await rp(url('/stateless')) + + expect(headers[reportOnlyHeader]).toMatch(/^script-src 'self' 'sha256-.*'/) + expect(headers[reportOnlyHeader]).toContain('https://example.com') + expect(headers[reportOnlyHeader]).toContain('https://example.io') + expect(headers[reportOnlyHeader]).toContain('report-uri /csp_report_uri') + } + ) + test( 'Contain Content-Security-Policy-Report-Only header, when csp.policies.script-src is not set', async () => {