diff --git a/files/nginx/odk.conf.template b/files/nginx/odk.conf.template index 937d006c6..09d2b04eb 100644 --- a/files/nginx/odk.conf.template +++ b/files/nginx/odk.conf.template @@ -186,7 +186,7 @@ server { # Rules set to 'none' here would fallback to default-src if excluded. # They are included here to ease interpretation of violation reports. - add_header Content-Security-Policy-Report-Only "default-src 'none'; connect-src 'self' https://translate.google.com https://translate.googleapis.com; font-src 'self'; frame-src 'self' https://getodk.github.io/central/news.html; img-src * data:; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; worker-src data:; report-uri /csp-report"; + add_header Content-Security-Policy-Report-Only "default-src 'none'; connect-src 'self' https://translate.google.com https://translate.googleapis.com; font-src 'self'; frame-src 'self' https://getodk.github.io/central/news.html; img-src * data: https://translate.google.com; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; worker-src data:; report-uri /csp-report"; include /usr/share/odk/nginx/common-headers.conf; } diff --git a/test/nginx/package-lock.json b/test/nginx/package-lock.json index 999cd3a54..c9e741e28 100644 --- a/test/nginx/package-lock.json +++ b/test/nginx/package-lock.json @@ -7,6 +7,7 @@ "name": "odk-central-tests", "dependencies": { "chai": "^5.2.0", + "deep-equal-in-any-order": "^2.1.0", "eslint": "^9.28.0", "mocha": "^11.6.0" } @@ -534,6 +535,16 @@ "node": ">=6" } }, + "node_modules/deep-equal-in-any-order": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/deep-equal-in-any-order/-/deep-equal-in-any-order-2.1.0.tgz", + "integrity": "sha512-9FklcFjcehm1yBWiOYtmazJOiMbT+v81Kq6nThIuXbWLWIZMX3ZI+QoLf7wCi0T8XzTAXf6XqEdEyVrjZkhbGA==", + "license": "MIT", + "dependencies": { + "lodash.mapvalues": "^4.6.0", + "sort-any": "^2.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1040,6 +1051,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1383,6 +1406,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sort-any": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-any/-/sort-any-2.0.0.tgz", + "integrity": "sha512-T9JoiDewQEmWcnmPn/s9h/PH9t3d/LSWi0RgVmXSuDYeZXTZOZ1/wrK2PHaptuR1VXe3clLLt0pD6sgVOwjNEA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/test/nginx/package.json b/test/nginx/package.json index 820affbb2..4355b1fef 100644 --- a/test/nginx/package.json +++ b/test/nginx/package.json @@ -7,6 +7,7 @@ }, "dependencies": { "chai": "^5.2.0", + "deep-equal-in-any-order": "^2.1.0", "eslint": "^9.28.0", "mocha": "^11.6.0" }, diff --git a/test/nginx/test-nginx.js b/test/nginx/test-nginx.js index 0f8033f93..f4cccd6ad 100644 --- a/test/nginx/test-nginx.js +++ b/test/nginx/test-nginx.js @@ -1,30 +1,46 @@ const https = require('node:https'); const tls = require('node:tls'); const { Readable } = require('stream'); -const { assert } = require('chai'); + +const deepEqualInAnyOrder = require('deep-equal-in-any-order'); +const chai = require('chai'); +chai.use(deepEqualInAnyOrder); +const { assert } = chai; const none = `'none'`; const self = `'self'`; const unsafeInline = `'unsafe-inline'`; + +const asArray = val => { + if (val == null) return []; + if (Array.isArray(val)) return val; + return [val]; +}; +const allowGoogleTranslate = ({ 'connect-src':connectSrc, 'img-src':imgSrc, ...others }) => ({ + ...others, + 'connect-src': [ + ...asArray(connectSrc), + 'https://translate.google.com', + 'https://translate.googleapis.com', + ], + 'img-src': [ + ...asArray(imgSrc), + 'https://translate.google.com', + ], +}); + const contentSecurityPolicies = { - 'backend-default': { + 'backend-default': allowGoogleTranslate({ 'default-src': none, - 'connect-src': [ - 'https://translate.google.com', - 'https://translate.googleapis.com', - ], - 'img-src': 'https://translate.google.com', 'report-uri': '/csp-report', - }, + }), 'backend-unmodified': { 'default-src': 'NOTE:FROM-BACKEND', }, - 'central-frontend': { + 'central-frontend': allowGoogleTranslate({ 'default-src': none, 'connect-src': [ self, - 'https://translate.google.com', - 'https://translate.googleapis.com', ], 'font-src': self, 'frame-src': [ @@ -40,12 +56,12 @@ const contentSecurityPolicies = { 'style-src-attr': unsafeInline, 'worker-src': 'data:', 'report-uri': '/csp-report', - }, + }), 'disallow-all': { 'default-src': none, 'report-uri': '/csp-report', }, - enketo: { + enketo: allowGoogleTranslate({ 'default-src': none, 'connect-src': [ self, @@ -55,8 +71,6 @@ const contentSecurityPolicies = { 'https://maps.gstatic.com/mapfiles/', 'https://fonts.gstatic.com/', 'https://fonts.googleapis.com/', - 'https://translate.google.com', - 'https://translate.googleapis.com', ], 'font-src': [ self, @@ -72,7 +86,6 @@ const contentSecurityPolicies = { 'https://maps.gstatic.com/mapfiles/', 'https://maps.googleapis.com/maps/', 'https://tile.openstreetmap.org/', - 'https://translate.google.com', ], 'manifest-src': none, 'media-src': [ @@ -95,7 +108,7 @@ const contentSecurityPolicies = { ], 'style-src-attr': unsafeInline, 'report-uri': '/csp-report', - }, + }), }; describe('nginx config', () => { @@ -864,5 +877,5 @@ function assertSecurityHeaders(res, { csp }) { const expectedCsp = contentSecurityPolicies[csp]; if(!expectedCsp) assert.fail(`Tried to match unknown CSP '${csp}'`); const actualCsp = res.headers.get('Content-Security-Policy-Report-Only'); - assert.deepEqual(actualCsp.split('; '), Object.entries(expectedCsp).map(([ k, v ]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`)); + assert.deepEqualInAnyOrder(actualCsp.split('; '), Object.entries(expectedCsp).map(([ k, v ]) => `${k} ${Array.isArray(v) ? v.join(' ') : v}`)); }