diff --git a/files/nginx/odk.conf.template b/files/nginx/odk.conf.template index 06193f499..b8b1e8728 100644 --- a/files/nginx/odk.conf.template +++ b/files/nginx/odk.conf.template @@ -130,8 +130,7 @@ server { } # To read single submission cookies location = /-/single/check-submitted { - alias /usr/share/nginx/html/blank.html; - default_type text/html; + try_files $uri @blank.html; } # For that iframe to work, we'll need another path prefix (enketo-passthrough) under which we can @@ -166,6 +165,17 @@ server { proxy_read_timeout 2m; } + location @blank.html { + root /usr/share/nginx/html; + try_files /blank.html =404; + + add_header Content-Security-Policy-Report-Only "default-src 'none'"; + include /usr/share/odk/nginx/common-headers.conf; + } + location = /blank.html { + try_files $uri @blank.html; + } + location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; diff --git a/test/nginx/test-nginx.js b/test/nginx/test-nginx.js index 0346491d0..8c336d1df 100644 --- a/test/nginx/test-nginx.js +++ b/test/nginx/test-nginx.js @@ -6,7 +6,7 @@ const none = `'none'`; const self = `'self'`; const unsafeInline = `'unsafe-inline'`; const contentSecurityPolicies = { - 'restrictive': { + 'backend-default': { 'default-src': none, 'connect-src': [ 'https://translate.google.com', @@ -37,6 +37,9 @@ const contentSecurityPolicies = { 'worker-src': 'data:', 'report-uri': '/csp-report', }, + 'disallow-all': { + 'default-src': none, + }, enketo: { 'default-src': none, 'connect-src': [ @@ -183,7 +186,6 @@ describe('nginx config', () => { [ [ '/index.html', /
<\/div>/ ], [ '/version.txt', /^versions:/ ], - [ '/blank.html', /^\n$/ ], [ '/favicon.ico', /^\n$/ ], ].forEach(([ path, expectedContent ]) => { it(`${path} file should serve expected content`, async () => { @@ -317,15 +319,23 @@ describe('nginx config', () => { }); }); - it('should serve blank page on /-/single/check-submitted', async () => { - // when - const res = await fetchHttps('/-/single/check-submitted'); + describe('blank.html', () => { + [ + '/blank.html', + '/-/single/check-submitted', + ].forEach(path => { + it(`should serve blank page on ${path}`, async () => { + // when + const res = await fetchHttps(path); - // then - assert.equal(res.status, 200); - assert.isEmpty((await res.text()).trim()); - assertSecurityHeaders(res, { csp:'restrictive' }); - await assertEnketoReceivedNoRequests(); + // then + assert.equal(res.status, 200); + assert.isEmpty((await res.text()).trim()); + assert.equal(res.headers.get('Content-Type'), 'text/html'); + assertSecurityHeaders(res, { csp:'disallow-all' }); + await assertEnketoReceivedNoRequests(); + }); + }); }); it('/v1/... should forward to backend', async () => { @@ -335,7 +345,7 @@ describe('nginx config', () => { // then assert.equal(res.status, 200); assert.equal(await res.text(), 'OK'); - assertSecurityHeaders(res, { csp:'restrictive' }); + assertSecurityHeaders(res, { csp:'backend-default' }); // and await assertBackendReceived( { method:'GET', path:'/v1/some/central-backend/path' }, @@ -347,7 +357,7 @@ describe('nginx config', () => { const res = await fetchHttps('/v1/reflect-headers'); // then assert.equal(res.status, 200); - assertSecurityHeaders(res, { csp:'restrictive' }); + assertSecurityHeaders(res, { csp:'backend-default' }); // when const body = await res.json(); @@ -365,7 +375,7 @@ describe('nginx config', () => { // then assert.equal(res.status, 200); // and - assertSecurityHeaders(res, { csp:'restrictive' }); + assertSecurityHeaders(res, { csp:'backend-default' }); // when const body = await res.json();