From 4dc1487163c84237afc76adc64de718b1e6b7bea Mon Sep 17 00:00:00 2001 From: Antoine Rousseau Date: Tue, 10 Sep 2019 22:21:50 +0200 Subject: [PATCH] security headers merging, fixes #8020 --- .../build-headers-program.js.snap | 77 +++++++++++++++++++ .../src/__tests__/build-headers-program.js | 21 +++++ .../src/build-headers-program.js | 31 +++++++- 3 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap b/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap index 283a009a4a80b..76134fb71e9c6 100644 --- a/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap +++ b/packages/gatsby-plugin-netlify/src/__tests__/__snapshots__/build-headers-program.js.snap @@ -76,6 +76,83 @@ exports[`with caching headers 1`] = ` " `; +exports[`with security headers 1`] = ` +"## Created with gatsby-plugin-netlify + +/* + X-Frame-Options: ALLOW-FROM https://app.storyblok.com/ + X-XSS-Protection: 1; mode=block + X-Content-Type-Options: nosniff + Referrer-Policy: same-origin + Content-Security-Policy: frame-ancestors 'self' https://*.storyblok.com/ +/component---node-modules-gatsby-plugin-offline-app-shell-js-78f9e4dea04737fa062d.js + Cache-Control: public, max-age=31536000, immutable +/0-0180cd94ef2497ac7db8.js + Cache-Control: public, max-age=31536000, immutable +/component---src-templates-blog-post-js-517987eae96e75cddbe7.js + Cache-Control: public, max-age=31536000, immutable +/component---src-pages-404-js-53e6c51a5a7e73090f50.js + Cache-Control: public, max-age=31536000, immutable +/component---src-pages-index-js-0bdd01c77ee09ef0224c.js + Cache-Control: public, max-age=31536000, immutable +/pages-manifest-ab11f09e0ca7ecd3b43e.js + Cache-Control: public, max-age=31536000, immutable +/webpack-runtime-acaa8994f1f704475e21.js + Cache-Control: public, max-age=31536000, immutable +/styles.1025963f4f2ec7abbad4.css + Cache-Control: public, max-age=31536000, immutable +/styles-565f081c8374bbda155f.js + Cache-Control: public, max-age=31536000, immutable +/app-f33c13590352da20930f.js + Cache-Control: public, max-age=31536000, immutable +/static/* + Cache-Control: public, max-age=31536000, immutable +/sw.js + Cache-Control: no-cache +/offline-plugin-app-shell-fallback/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/hi-folks/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/my-second-post/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/hello-world/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/404/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/ + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +/404.html + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script + Link: ; rel=preload; as=script +" +`; + exports[`without caching headers 1`] = ` "## Created with gatsby-plugin-netlify diff --git a/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js b/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js index 89af39f8f02a0..018ac8f0f2df5 100644 --- a/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js +++ b/packages/gatsby-plugin-netlify/src/__tests__/build-headers-program.js @@ -177,3 +177,24 @@ test(`without caching headers`, async () => { await fs.readFile(pluginData.publicFolder(`_headers`), `utf8`) ).toMatchSnapshot() }) + +test(`with security headers`, async () => { + const pluginData = await createPluginData() + + const pluginOptions = { + ...DEFAULT_OPTIONS, + mergeSecurityHeaders: true, + headers: { + "/*": [ + `Content-Security-Policy: frame-ancestors 'self' https://*.storyblok.com/`, + `X-Frame-Options: ALLOW-FROM https://app.storyblok.com/`, + ], + }, + } + + await buildHeadersProgram(pluginData, pluginOptions) + + expect( + await fs.readFile(pluginData.publicFolder(`_headers`), `utf8`) + ).toMatchSnapshot() +}) diff --git a/packages/gatsby-plugin-netlify/src/build-headers-program.js b/packages/gatsby-plugin-netlify/src/build-headers-program.js index 4f7d26addc52f..d51c5f62b80b2 100644 --- a/packages/gatsby-plugin-netlify/src/build-headers-program.js +++ b/packages/gatsby-plugin-netlify/src/build-headers-program.js @@ -12,6 +12,11 @@ import { NETLIFY_HEADERS_FILENAME, } from "./constants" +function getHeaderName(header) { + const matches = header.match(/^([^:]+):/) + return matches && matches[1] +} + function validHeaders(headers) { if (!headers || !_.isObject(headers)) { return false @@ -21,7 +26,10 @@ function validHeaders(headers) { headers, (headersList, path) => _.isArray(headersList) && - _.every(headersList, header => _.isString(header)) + _.every( + headersList, + header => _.isString(header) && getHeaderName(header) + ) ) } @@ -97,6 +105,25 @@ function defaultMerge(...headers) { return _.mergeWith({}, ...headers, unionMerge) } +function headersMerge(userHeaders, defaultHeaders) { + const merged = {} + Object.keys(defaultHeaders).forEach(path => { + if (!userHeaders[path]) { + merged[path] = defaultHeaders[path] + return + } + const headersMap = {} + defaultHeaders[path].forEach(header => { + headersMap[getHeaderName(header)] = header + }) + userHeaders[path].forEach(header => { + headersMap[getHeaderName(header)] = header // override if exists + }) + merged[path] = Object.values(headersMap) + }) + return merged +} + function transformLink(manifest, publicFolder, pathPrefix) { return header => header.replace(LINK_REGEX, (__, prefix, file, suffix) => { @@ -216,7 +243,7 @@ const applySecurityHeaders = ({ mergeSecurityHeaders }) => headers => { return headers } - return defaultMerge(headers, SECURITY_HEADERS) + return headersMerge(headers, SECURITY_HEADERS) } const applyCachingHeaders = (