Merge pull request #19590 from med8bra/fix/strapi-csp-merge

fix(security-middleware): fix config merging with defaults
This commit is contained in:
Alexandre BODIN 2024-06-03 10:10:30 +02:00 committed by GitHub
commit 1b03d7e2c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 3 deletions

View File

@ -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');
});
});
});
});

View File

@ -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> =
(config, { strapi }) =>
(ctx, next) => {
@ -42,7 +50,7 @@ export const security: Common.MiddlewareFactory<Config> =
}
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<Config> =
}
if (ctx.method === 'GET' && ['/admin'].some((str) => ctx.path.startsWith(str))) {
helmetConfig = merge(helmetConfig, {
helmetConfig = mergeConfig(helmetConfig, {
contentSecurityPolicy: {
directives: {
'script-src': ["'self'", "'unsafe-inline'"],