From 4ae5b2f502f35658c59c90e0a2082a1d9cc44dbb Mon Sep 17 00:00:00 2001 From: med8bra Date: Fri, 23 Feb 2024 02:03:46 +0100 Subject: [PATCH] fix(security-middleware): fix config merging with defaults --- .../middlewares/__tests__/security.test.ts | 65 +++++++++++++++++++ .../core/strapi/src/middlewares/security.ts | 14 +++- 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 packages/core/strapi/src/middlewares/__tests__/security.test.ts diff --git a/packages/core/strapi/src/middlewares/__tests__/security.test.ts b/packages/core/strapi/src/middlewares/__tests__/security.test.ts new file mode 100644 index 0000000000..a242c4de9b --- /dev/null +++ b/packages/core/strapi/src/middlewares/__tests__/security.test.ts @@ -0,0 +1,65 @@ +import Koa from 'koa'; +import request from 'supertest'; +import { security } from '../security'; + +const parseCspHeader = (csp: string) => + Object.fromEntries( + csp + .split(';') + .map((directive) => directive.split(' ')) + .map(([k, ...v]) => [k, v]) + ); + +describe('Security middleware', () => { + describe('Content security policy', () => { + // GIVEN + const app = new Koa(); + const securityMiddleware = security( + { + contentSecurityPolicy: { + useDefaults: true, + directives: { + 'script-src': ["'self'", 'https://cdn.custom.com'], + upgradeInsecureRequests: null, + }, + }, + }, + { + strapi: { + plugin: () => null, + } as any, + } + )!; + + // WHEN + app.use(securityMiddleware); + const agent = request.agent(app.callback()); + + // THEN + it.each(['/', '/admin', '/api'])( + 'includes user custom CSP directives in GET %s response', + async (path) => { + await agent.get(path).expect((req) => { + const csp = parseCspHeader(req.header['content-security-policy']); + expect(csp['script-src']).toContain('https://cdn.custom.com'); + }); + } + ); + it('includes required default CSP directives in GET /admin response', async () => { + await agent.get('/admin').expect((req) => { + const csp = parseCspHeader(req.header['content-security-policy']); + expect(csp['script-src']).toContain("'unsafe-inline'"); + expect(csp['connect-src']).toContain('ws:'); + }); + }); + it('includes required default CSP directives in GET /documentation response', async () => { + await agent.get('/documentation').expect((req) => { + const csp = parseCspHeader(req.header['content-security-policy']); + expect(csp['script-src']).toContain("'unsafe-inline'"); + expect(csp['script-src']).toContain('cdn.jsdelivr.net'); + expect(csp['img-src']).toContain('strapi.io'); + expect(csp['img-src']).toContain('cdn.jsdelivr.net'); + }); + }); + }); +}); diff --git a/packages/core/strapi/src/middlewares/security.ts b/packages/core/strapi/src/middlewares/security.ts index 13c645ac31..6c886437e0 100644 --- a/packages/core/strapi/src/middlewares/security.ts +++ b/packages/core/strapi/src/middlewares/security.ts @@ -1,4 +1,4 @@ -import { defaultsDeep, merge } from 'lodash/fp'; +import { defaultsDeep, mergeWith } from 'lodash/fp'; import helmet, { KoaHelmet } from 'koa-helmet'; import type { Common } from '@strapi/types'; @@ -29,6 +29,14 @@ const defaults: Config = { }, }; +const mergeConfig = (existingConfig: Config, newConfig: Config) => { + return mergeWith( + (obj, src) => (Array.isArray(obj) && Array.isArray(src) ? obj.concat(src) : undefined), + existingConfig, + newConfig + ); +}; + export const security: Common.MiddlewareFactory = (config, { strapi }) => (ctx, next) => { @@ -42,7 +50,7 @@ export const security: Common.MiddlewareFactory = } if (ctx.method === 'GET' && specialPaths.some((str) => ctx.path.startsWith(str))) { - helmetConfig = merge(helmetConfig, { + helmetConfig = mergeConfig(helmetConfig, { contentSecurityPolicy: { directives: { 'script-src': ["'self'", "'unsafe-inline'", 'cdn.jsdelivr.net'], @@ -53,7 +61,7 @@ export const security: Common.MiddlewareFactory = } if (ctx.method === 'GET' && ['/admin'].some((str) => ctx.path.startsWith(str))) { - helmetConfig = merge(helmetConfig, { + helmetConfig = mergeConfig(helmetConfig, { contentSecurityPolicy: { directives: { 'script-src': ["'self'", "'unsafe-inline'"],