feat(security): enforce the Content Security Policy

The report-only run showed no violation in production.
This commit is contained in:
lobo committed 2026-09-26 07:15:14 +02:00
1 parent c52c5e02d2
commit 7d9cf907fc
3 files changed
+8 -5

No files matched your search

+1 -2
View File
@@ -47,7 +47,7 @@ To Be Played is split into three repositories:
- react-hook-form + zod for forms, i18next for translations (`public/i18n`)
- Vitest + React Testing Library + jsdom
- Production image: nginx (Alpine), SPA fallback, long-term caching for hashed assets, Content Security Policy
(report-only for now) and security headers
and security headers
- CI: Forgejo Actions (`npm ci`, `npm test`, `npm run build`, Docker image)
## Project structure
@@ -91,7 +91,6 @@ npm run build
## Roadmap
- Mood filter for "What to play next".
- Enforce the Content Security Policy (it runs in report-only mode now).
## License
+1 -2
View File
@@ -3,8 +3,7 @@ server {
root /usr/share/nginx/html;
# An nginx template: AUTH_HOST and API_HOST come from the container env at start (see Dockerfile).
# Report-Only until production shows no violation in the browser console; then rename the header.
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.steamstatic.com https://steamcdn-a.akamaihd.net https://media.steampowered.com https://images.igdb.com; font-src 'self' data:; connect-src 'self' https://${AUTH_HOST} https://${API_HOST}; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.steamstatic.com https://steamcdn-a.akamaihd.net https://media.steampowered.com https://images.igdb.com; font-src 'self' data:; connect-src 'self' https://${AUTH_HOST} https://${API_HOST}; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
+6 -1
View File
@@ -4,7 +4,7 @@ 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 policy = nginx.match(/Content-Security-Policy "([^"]+)"/)[1];
const directive = (name) => policy.split(';').map((part) => part.trim()).find((part) => part.startsWith(`${name} `));
const LINK_ONLY_HOSTS = ['store.steampowered.com'];
@@ -34,6 +34,11 @@ describe('Content Security Policy in nginx.conf', () => {
expect(directive('connect-src')).toBe("connect-src 'self' https://${AUTH_HOST} https://${API_HOST}");
});
it('is enforced, not only reported', () => {
expect(nginx).toMatch(/add_header Content-Security-Policy "/);
expect(nginx).not.toContain('Content-Security-Policy-Report-Only');
});
it('allows no inline or foreign script', () => {
expect(directive('script-src')).toBe("script-src 'self'");
expect(directive('frame-ancestors')).toBe("frame-ancestors 'none'");