From 077fbc3266e07e98f1a1072056a38b2b72b6a9a4 Mon Sep 17 00:00:00 2001 From: Jonny Adshead Date: Thu, 29 Jun 2023 12:35:44 -0700 Subject: [PATCH] fix(csp-report): add application/csp-report content type support (#1038) Co-authored-by: Matthew Mallimo --- .../__snapshots__/one-app.spec.js.snap | 2 +- __tests__/integration/one-app.spec.js | 2 +- __tests__/server/ssrServer-requests.spec.js | 124 ++++++++++++++++++ __tests__/server/ssrServer.spec.js | 18 +-- src/server/ssrServer.js | 11 +- 5 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 __tests__/server/ssrServer-requests.spec.js diff --git a/__tests__/integration/__snapshots__/one-app.spec.js.snap b/__tests__/integration/__snapshots__/one-app.spec.js.snap index b594e597..7608519d 100644 --- a/__tests__/integration/__snapshots__/one-app.spec.js.snap +++ b/__tests__/integration/__snapshots__/one-app.spec.js.snap @@ -92,4 +92,4 @@ exports[`Tests that require Docker setup one-app successfully started metrics ha exports[`Tests that require Docker setup one-app successfully started one-app server provides reporting routes client reported errors logs errors when reported to /_/report/errors 1`] = `"reported client error"`; -exports[`Tests that require Docker setup one-app successfully started one-app server provides reporting routes csp-violations reported to server logs violations reported to /_/report/errors 1`] = `"CSP Violation: {\\n \\"csp-report\\": {\\n \\"document-uri\\": \\"bad.example.com"`; +exports[`Tests that require Docker setup one-app successfully started one-app server provides reporting routes csp-violations reported to server logs violations reported to /_/report/errors 1`] = `"CSP Violation: {\\"csp-report\\":{\\"document-uri\\":\\"bad.example.com"`; diff --git a/__tests__/integration/one-app.spec.js b/__tests__/integration/one-app.spec.js index 6e6544e2..812786ba 100644 --- a/__tests__/integration/one-app.spec.js +++ b/__tests__/integration/one-app.spec.js @@ -273,7 +273,7 @@ describe('Tests that require Docker setup', () => { ...defaultFetchOptions, method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/csp-report', }, body: JSON.stringify({ 'csp-report': { diff --git a/__tests__/server/ssrServer-requests.spec.js b/__tests__/server/ssrServer-requests.spec.js new file mode 100644 index 00000000..a361acab --- /dev/null +++ b/__tests__/server/ssrServer-requests.spec.js @@ -0,0 +1,124 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import ssrServer from '../../src/server/ssrServer'; + +const { NODE_ENV } = process.env; +const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + +jest.mock('fastify-metrics', () => (_req, _opts, done) => done()); + +describe('ssrServer route testing', () => { + afterAll(() => { + process.env.NODE_ENV = NODE_ENV; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('/_/status', async () => { + const server = await ssrServer(); + const resp = await server.inject({ + method: 'GET', + url: '/_/status', + }); + + expect(resp.statusCode).toEqual(200); + expect(resp.body).toEqual('OK'); + }); + + describe('/_/report/security/csp-violation', () => { + describe('production', () => { + let server; + beforeAll(async () => { + process.env.NODE_ENV = 'production'; + server = await ssrServer(); + }); + + test('with csp-report', async () => { + const resp = await server.inject({ + method: 'POST', + url: '/_/report/security/csp-violation', + headers: { + 'Content-Type': 'application/csp-report', + }, + payload: JSON.stringify({ + 'csp-report': { + 'document-uri': 'bad.example.com', + }, + }), + }); + expect(warnSpy).toHaveBeenCalledWith('CSP Violation: {"csp-report":{"document-uri":"bad.example.com"}}'); + expect(resp.statusCode).toEqual(204); + }); + + test('when csp-report is not provided', async () => { + const resp = await server.inject({ + method: 'POST', + url: '/_/report/security/csp-violation', + headers: { + 'Content-Type': 'application/csp-report', + }, + }); + expect(warnSpy).toHaveBeenCalledWith('CSP Violation: No data received!'); + expect(resp.statusCode).toEqual(204); + }); + }); + + describe('development', () => { + let server; + beforeAll(async () => { + process.env.NODE_ENV = 'development'; + server = await ssrServer(); + }); + + test('with csp-report', async () => { + const resp = await server.inject({ + method: 'POST', + url: '/_/report/security/csp-violation', + headers: { + 'Content-Type': 'application/csp-report', + }, + payload: JSON.stringify({ + 'csp-report': { + 'document-uri': 'bad.example.com', + 'violated-directive': 'script-src', + 'blocked-uri': 'blockedUri.example.com', + 'line-number': '123', + 'column-number': '432', + 'source-file': 'sourceFile.js', + }, + }), + }); + expect(warnSpy).toHaveBeenCalledWith('CSP Violation: sourceFile.js:123:432 on page bad.example.com violated the script-src policy via blockedUri.example.com'); + expect(resp.statusCode).toEqual(204); + }); + + test('when no csp-report', async () => { + const resp = await server.inject({ + method: 'POST', + url: '/_/report/security/csp-violation', + headers: { + 'Content-Type': 'application/csp-report', + }, + }); + expect(warnSpy).toHaveBeenCalledWith('CSP Violation reported, but no data received'); + expect(resp.statusCode).toEqual(204); + }); + }); + }); +}); diff --git a/__tests__/server/ssrServer.spec.js b/__tests__/server/ssrServer.spec.js index da21c007..9bd35ab0 100644 --- a/__tests__/server/ssrServer.spec.js +++ b/__tests__/server/ssrServer.spec.js @@ -107,6 +107,7 @@ describe('ssrServer', () => { setNotFoundHandler, setErrorHandler, ready, + addContentTypeParser: jest.fn(), })); }); @@ -325,7 +326,10 @@ describe('ssrServer', () => { }, null, jest.fn()); const request = { - body: {}, + headers: { + 'Content-Type': 'application/csp-report', + }, + body: JSON.stringify({}), }; const reply = { status: jest.fn(() => reply), @@ -353,7 +357,7 @@ describe('ssrServer', () => { }, null, jest.fn()); const request = { - body: { + body: JSON.stringify({ 'csp-report': { 'document-uri': 'document-uri', 'violated-directive': 'violated-directive', @@ -362,7 +366,7 @@ describe('ssrServer', () => { 'column-number': 'column-number', 'source-file': 'source-file', }, - }, + }), }; const reply = { status: jest.fn(() => reply), @@ -450,9 +454,9 @@ describe('ssrServer', () => { const request = { headers: {}, - body: { + body: JSON.stringify({ unit: 'testing', - }, + }), }; const reply = { status: jest.fn(() => reply), @@ -462,9 +466,7 @@ describe('ssrServer', () => { post.mock.calls[0][1](request, reply); expect(post.mock.calls[0][0]).toEqual('/_/report/security/csp-violation'); - expect(console.warn).toHaveBeenCalledWith(`CSP Violation: { - "unit": "testing" -}`); + expect(console.warn).toHaveBeenCalledWith('CSP Violation: {"unit":"testing"}'); expect(reply.status).toHaveBeenCalledWith(204); expect(reply.send).toHaveBeenCalled(); }); diff --git a/src/server/ssrServer.js b/src/server/ssrServer.js index 454ca24d..a599643f 100644 --- a/src/server/ssrServer.js +++ b/src/server/ssrServer.js @@ -103,16 +103,19 @@ export async function createApp(opts = {}) { done(); }); - // PWA + fastify.addContentTypeParser('application/csp-report', { parseAs: 'string' }, (req, body, doneParsing) => { + doneParsing(null, body); + }); + + // pwa manifest & Report routes for csp and errors fastify.register((instance, _opts, done) => { instance.register(addCacheHeaders); instance.register(csp); - instance.get('/_/pwa/manifest.webmanifest', webManifestMiddleware); if (nodeEnvIsDevelopment()) { instance.post('/_/report/security/csp-violation', (request, reply) => { - const violation = request.body && request.body['csp-report']; + const violation = request.body && JSON.parse(request.body)['csp-report']; if (!violation) { console.warn('CSP Violation reported, but no data received'); } else { @@ -131,7 +134,7 @@ export async function createApp(opts = {}) { }); } else { instance.post('/_/report/security/csp-violation', (request, reply) => { - const violation = request.body ? JSON.stringify(request.body, null, 2) : 'No data received!'; + const violation = request.body ? request.body : 'No data received!'; console.warn(`CSP Violation: ${violation}`); reply.status(204).send(); });