diff --git a/package-lock.json b/package-lock.json index 6991634b04..a0ea0409aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35741,7 +35741,7 @@ "fuse.js": "^3.0.0", "globby": "^5.0.0", "handlebars": "^4.7.7", - "helmet": "^3.22.0", + "helmet": "^6.0.1", "http-proxy": "^1.18.1", "i18next": "^19.6.3", "i18next-fs-backend": "^1.0.7", @@ -37189,6 +37189,14 @@ "node": ">=12.0.0" } }, + "services/web/node_modules/helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==", + "engines": { + "node": ">=14.0.0" + } + }, "services/web/node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -46214,7 +46222,7 @@ "globby": "^5.0.0", "handlebars": "^4.7.7", "handlebars-loader": "^1.7.1", - "helmet": "^3.22.0", + "helmet": "^6.0.1", "html-webpack-plugin": "^5.5.0", "http-proxy": "^1.18.1", "i18next": "^19.6.3", @@ -47262,6 +47270,11 @@ "jws": "^4.0.0" } }, + "helmet": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.0.1.tgz", + "integrity": "sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==" + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", diff --git a/services/web/app/src/infrastructure/Server.js b/services/web/app/src/infrastructure/Server.js index dbb3c84c33..086719b7c0 100644 --- a/services/web/app/src/infrastructure/Server.js +++ b/services/web/app/src/infrastructure/Server.js @@ -269,6 +269,14 @@ webRouter.use( dnsPrefetchControl: false, referrerPolicy: { policy: 'origin-when-cross-origin' }, hsts: false, + // Disabled because it's impractical to include every resource via CORS or + // with the magic CORP header + crossOriginEmbedderPolicy: false, + // Disabled because it's not a security header and has possibly-unwanted + // effects + originAgentCluster: false, + // We have custom handling for CSP below, so Helmet's default is disabled + contentSecurityPolicy: false, }) ) diff --git a/services/web/package.json b/services/web/package.json index c8570c43fe..fb4a7efb6c 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -164,7 +164,7 @@ "fuse.js": "^3.0.0", "globby": "^5.0.0", "handlebars": "^4.7.7", - "helmet": "^3.22.0", + "helmet": "^6.0.1", "http-proxy": "^1.18.1", "i18next": "^19.6.3", "i18next-fs-backend": "^1.0.7", diff --git a/services/web/test/acceptance/src/SecurityHeadersTests.js b/services/web/test/acceptance/src/SecurityHeadersTests.js index ea6d3e83d3..8f8b0c1bec 100644 --- a/services/web/test/acceptance/src/SecurityHeadersTests.js +++ b/services/web/test/acceptance/src/SecurityHeadersTests.js @@ -19,32 +19,38 @@ const request = require('./helpers/request') const assert_has_common_headers = function (response) { const { headers } = response - assert.equal(headers['x-download-options'], 'noopen') - assert.equal(headers['x-xss-protection'], '1; mode=block') - return assert.equal(headers['referrer-policy'], 'origin-when-cross-origin') + assert.include(headers, { + 'x-download-options': 'noopen', + 'x-xss-protection': '0', + 'cross-origin-resource-policy': 'same-origin', + 'cross-origin-opener-policy': 'same-origin', + 'x-content-type-options': 'nosniff', + 'x-permitted-cross-domain-policies': 'none', + 'referrer-policy': 'origin-when-cross-origin', + }) + assert.isUndefined(headers['cross-origin-embedder-policy']) } const assert_has_cache_headers = function (response) { - const { headers } = response - assert.equal(headers['surrogate-control'], 'no-store') - assert.equal( - headers['cache-control'], - 'no-store, no-cache, must-revalidate, proxy-revalidate' - ) - assert.equal(headers.pragma, 'no-cache') - return assert.equal(headers.expires, '0') + assert.include(response.headers, { + 'surrogate-control': 'no-store', + 'cache-control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + pragma: 'no-cache', + expires: '0', + }) } const assert_has_no_cache_headers = function (response) { - const { headers } = response - assert.isUndefined(headers['surrogate-control']) - assert.isUndefined(headers['cache-control']) - assert.isUndefined(headers.pragma) - return assert.isUndefined(headers.expires) + assert.doesNotHaveAnyKeys(response.headers, [ + 'surrogate-control', + 'cache-control', + 'pragma', + 'expires', + ]) } + const assert_has_asset_caching_headers = function (response) { - const { headers } = response - assert.equal(headers['cache-control'], 'public, max-age=31536000') + assert.equal(response.headers['cache-control'], 'public, max-age=31536000') } describe('SecurityHeaders', function () {