From 4ae5b2f502f35658c59c90e0a2082a1d9cc44dbb Mon Sep 17 00:00:00 2001 From: med8bra Date: Fri, 23 Feb 2024 02:03:46 +0100 Subject: [PATCH 1/2] 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'"], From 28515f333803ef8ef5ccb671171cac96399a4412 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 3 Jun 2024 10:30:06 +0200 Subject: [PATCH 2/2] chore(ci): remove mysql_native_password in test container (#20400) --- .github/workflows/tests.yml | 2 -- docker-compose.test.yml | 1 - 2 files changed, 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d12d3e966a..35ce7a2ad0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -295,7 +295,6 @@ jobs: MYSQL_USER: strapi MYSQL_PASSWORD: strapi MYSQL_DATABASE: strapi_test - MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password options: >- --health-cmd="mysqladmin ping" --health-interval=10s @@ -450,7 +449,6 @@ jobs: MYSQL_USER: strapi MYSQL_PASSWORD: strapi MYSQL_DATABASE: strapi_test - MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password options: >- --health-cmd="mysqladmin ping" --health-interval=10s diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d0b9118daa..f4093015e9 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -16,7 +16,6 @@ services: mysql: image: mysql restart: always - command: --default-authentication-plugin=mysql_native_password environment: MYSQL_DATABASE: strapi_test MYSQL_USER: strapi