feat(security): add a report-only CSP and security headers
Build, Test & Push Frontend / quality-check (push) Successful in 14s
Build, Test & Push Frontend / docker (push) Successful in 23s

nginx.conf is now an nginx template, so connect-src takes
AUTH_HOST and API_HOST from the container env. The image
sets both to empty by default, so nginx still starts
without them. Report-only until production shows no
violation. Also nosniff, Referrer-Policy and
Permissions-Policy.
This commit is contained in:
lobo committed 2026-09-25 15:47:23 +02:00
1 parent f8e6c2c970
commit 6edb53162e
4 files changed
+56 -3

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import {readdirSync, readFileSync} from 'node:fs';
import path from 'node:path';
import {describe, expect, it} from 'vitest';
const root = path.resolve(import.meta.dirname, '..');
const nginx = readFileSync(path.join(root, 'nginx.conf'), 'utf8');
const policy = nginx.match(/Content-Security-Policy(?:-Report-Only)? "([^"]+)"/)[1];
const directive = (name) => policy.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name} `));
const LINK_ONLY_HOSTS = ['store.steampowered.com'];
const allows = (sources, host) => sources.split(' ').some((source) =>
source === `https://${host}` || (source.startsWith('https://*.') && host.endsWith(source.slice('https://*'.length))));
describe('Content Security Policy in nginx.conf', () => {
it('allows every image host the code builds URLs for', () => {
const code = readdirSync(path.join(root, 'src'), {recursive: true})
.filter((file) => /\.jsx?$/.test(file) && !/\.test\.jsx?$/.test(file) && !file.startsWith('test'))
.map((file) => readFileSync(path.join(root, 'src', file), 'utf8'))
.join('\n');
const hosts = new Set([...code.matchAll(/`https:\/\/([a-z0-9.-]+)\//g)].map((match) => match[1]));
// Only links: a navigation needs no img-src.
LINK_ONLY_HOSTS.forEach((host) => hosts.delete(host));
// Hosts the backends pass on: Steam avatars and achievement icons, IGDB pictures.
['avatars.steamstatic.com', 'steamcdn-a.akamaihd.net', 'media.steampowered.com', 'images.igdb.com']
.forEach((host) => hosts.add(host));
for (const host of hosts) {
expect(allows(directive('img-src'), host), host).toBe(true);
}
});
it('lets the app reach Auth and Core, whose hosts come from the container env', () => {
expect(directive('connect-src')).toBe("connect-src 'self' https://${AUTH_HOST} https://${API_HOST}");
});
it('allows no inline or foreign script', () => {
expect(directive('script-src')).toBe("script-src 'self'");
expect(directive('frame-ancestors')).toBe("frame-ancestors 'none'");
});
});