From f0d77079176060759240999c1106a9c9f67a16af Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Thu, 19 Feb 2026 09:04:47 +0100 Subject: [PATCH 1/5] adding csp --- next.config.mjs => next.config.ts | 32 +++++++++++--- src/lib/csp.ts | 71 +++++++++++++++++++++++++++++++ src/lib/highlight/client.ts | 3 +- 3 files changed, 99 insertions(+), 7 deletions(-) rename next.config.mjs => next.config.ts (71%) create mode 100644 src/lib/csp.ts diff --git a/next.config.mjs b/next.config.ts similarity index 71% rename from next.config.mjs rename to next.config.ts index 313d8c8a..e0f56301 100644 --- a/next.config.mjs +++ b/next.config.ts @@ -1,9 +1,28 @@ import createMDX from '@next/mdx'; +import { getContentSecurityPolicyHeaderValue } from '@/lib/csp'; /** @type {import('next').NextConfig} */ const nextConfig = { pageExtensions: ['mdx', 'tsx'], + async headers() { + const cspValue = getContentSecurityPolicyHeaderValue(); + return [ + { + source: '/(.*)', + headers: [ + { + key: 'Content-Security-Policy', + value: cspValue, + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + ], + }, + ]; + }, experimental: { optimizePackageImports: ['@/components', '@/markdown', '@/icons'], turbo: { @@ -41,15 +60,16 @@ const rehypeAutolinkHeadings = { width: 16, viewBox: '0 0 16 16', }, - children: [{ - type: 'element', - tagName: 'use', - properties: { href: '#action/link' } - }] + children: [ + { + type: 'element', + tagName: 'use', + properties: { href: '#action/link' }, + }, + ], }, }; - const withMDX = createMDX({ options: { remarkPlugins: [ diff --git a/src/lib/csp.ts b/src/lib/csp.ts new file mode 100644 index 00000000..186f60e8 --- /dev/null +++ b/src/lib/csp.ts @@ -0,0 +1,71 @@ +export const getContentSecurityPolicyHeaderValue = () => { + const sentryReportUrl = process.env.SENTRY_SECURITY_REPORT_URL; + + const definitions: Record> = { + defaults: { + 'default-src': [`'self'`], + 'script-src': [ + `'self'`, + // Required for Next.js inline hydration scripts in static output + `'unsafe-inline'`, + // Required for syntax highlighting + `'wasm-unsafe-eval'`, + ], + 'style-src': [ + `'self'`, + // Required for Next.js inline styles + `'unsafe-inline'`, + ], + 'img-src': [`'self'`, 'data:'], + 'connect-src': [`'self'`], + 'form-action': [`'self'`], + 'frame-src': [`'none'`], + 'font-src': [`'self'`], + 'child-src': [`'self'`], + 'media-src': [`'self'`], + 'object-src': [`'none'`], + 'base-uri': [`'none'`], + }, + cloudsmith: { + 'connect-src': ['https://api.cloudsmith.io'], + }, + simpleAnalytics: { + 'script-src': ['https://simple.cloudsmith.com'], + 'connect-src': [ + 'https://queue.simpleanalyticscdn.com', + 'https://simple.cloudsmith.io', + 'https://simple.cloudsmith.com', + ], + 'img-src': ['https://queue.simpleanalyticscdn.com', 'https://simple.cloudsmith.com'], + }, + vercel: { + 'script-src': ['https://va.vercel-scripts.com'], + 'connect-src': ['https://va.vercel-scripts.com'], + }, + }; + + if (sentryReportUrl) { + definitions.defaults['report-uri'] = [sentryReportUrl]; + definitions.defaults['report-to'] = ['csp-endpoint']; + } + + const directives: Record = {}; + + for (const source in definitions) { + for (const directive in definitions[source]) { + if (!directives[directive]) { + directives[directive] = []; + } + directives[directive].push(definitions[source][directive]); + } + } + + let cspValue = ''; + + for (const directive in directives) { + const flattenedValues = Array.from(new Set(directives[directive].flat().filter(Boolean))); + cspValue += `${directive} ${flattenedValues.join(' ')}; `; + } + + return cspValue.trim(); +}; diff --git a/src/lib/highlight/client.ts b/src/lib/highlight/client.ts index 40e797f5..eda47c22 100644 --- a/src/lib/highlight/client.ts +++ b/src/lib/highlight/client.ts @@ -17,7 +17,8 @@ export const useHighlighter = () => { setHighlighter(h); setFetching(false); }) - .catch(() => { + .catch((e) => { + console.log(e); setError(true); setFetching(false); }); From d2b38692e25477a6c1b2cb9ad2b54ec3fce76eea Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Thu, 19 Feb 2026 09:06:00 +0100 Subject: [PATCH 2/5] clean up logs --- src/lib/highlight/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/highlight/client.ts b/src/lib/highlight/client.ts index eda47c22..9d391b0a 100644 --- a/src/lib/highlight/client.ts +++ b/src/lib/highlight/client.ts @@ -18,7 +18,6 @@ export const useHighlighter = () => { setFetching(false); }) .catch((e) => { - console.log(e); setError(true); setFetching(false); }); From 64e721dcd21d946b68caccb75e238abc68a6f712 Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Thu, 19 Feb 2026 09:12:25 +0100 Subject: [PATCH 3/5] final clean up --- src/lib/highlight/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/highlight/client.ts b/src/lib/highlight/client.ts index 9d391b0a..40e797f5 100644 --- a/src/lib/highlight/client.ts +++ b/src/lib/highlight/client.ts @@ -17,7 +17,7 @@ export const useHighlighter = () => { setHighlighter(h); setFetching(false); }) - .catch((e) => { + .catch(() => { setError(true); setFetching(false); }); From f198aa1e268eb70db98991fd116be9185ccb459a Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Thu, 19 Feb 2026 09:19:16 +0100 Subject: [PATCH 4/5] adding test --- src/lib/csp.test.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/lib/csp.ts | 7 ------ 2 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 src/lib/csp.test.ts diff --git a/src/lib/csp.test.ts b/src/lib/csp.test.ts new file mode 100644 index 00000000..fa756838 --- /dev/null +++ b/src/lib/csp.test.ts @@ -0,0 +1,53 @@ +import { getContentSecurityPolicyHeaderValue } from './csp'; + +describe('lib', () => { + describe('csp.ts', () => { + describe('getContentSecurityPolicyHeaderValue()', () => { + test('returns a trimmed string', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toBe(result.trim()); + }); + + test('includes default-src self', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toContain("default-src 'self'"); + }); + + test('includes required script-src sources', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toContain("'unsafe-inline'"); + expect(result).toContain("'wasm-unsafe-eval'"); + expect(result).toContain('https://simple.cloudsmith.com'); + expect(result).toContain('https://va.vercel-scripts.com'); + }); + + test('includes required connect-src sources', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toContain('https://api.cloudsmith.io'); + expect(result).toContain('https://queue.simpleanalyticscdn.com'); + expect(result).toContain('https://simple.cloudsmith.io'); + expect(result).toContain('https://simple.cloudsmith.com'); + }); + + test('sets frame-src and object-src to none', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toContain("frame-src 'none'"); + expect(result).toContain("object-src 'none'"); + }); + + test('sets base-uri to none', () => { + const result = getContentSecurityPolicyHeaderValue(); + expect(result).toContain("base-uri 'none'"); + }); + + test('deduplicates values within a directive', () => { + const result = getContentSecurityPolicyHeaderValue(); + const scriptSrcMatch = result.match(/script-src ([^;]+)/); + expect(scriptSrcMatch).not.toBeNull(); + const values = scriptSrcMatch![1].trim().split(' '); + const unique = new Set(values); + expect(values.length).toBe(unique.size); + }); + }); + }); +}); diff --git a/src/lib/csp.ts b/src/lib/csp.ts index 186f60e8..34e72d63 100644 --- a/src/lib/csp.ts +++ b/src/lib/csp.ts @@ -1,6 +1,4 @@ export const getContentSecurityPolicyHeaderValue = () => { - const sentryReportUrl = process.env.SENTRY_SECURITY_REPORT_URL; - const definitions: Record> = { defaults: { 'default-src': [`'self'`], @@ -44,11 +42,6 @@ export const getContentSecurityPolicyHeaderValue = () => { }, }; - if (sentryReportUrl) { - definitions.defaults['report-uri'] = [sentryReportUrl]; - definitions.defaults['report-to'] = ['csp-endpoint']; - } - const directives: Record = {}; for (const source in definitions) { From 6a23b7f2cf1d1440cde4d8429fa50f88c5af48f9 Mon Sep 17 00:00:00 2001 From: Rune Madsen Date: Thu, 19 Feb 2026 10:15:37 +0100 Subject: [PATCH 5/5] adding qualified --- src/lib/csp.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/csp.ts b/src/lib/csp.ts index 34e72d63..b1afc6bb 100644 --- a/src/lib/csp.ts +++ b/src/lib/csp.ts @@ -27,6 +27,11 @@ export const getContentSecurityPolicyHeaderValue = () => { cloudsmith: { 'connect-src': ['https://api.cloudsmith.io'], }, + qualified: { + 'script-src': ['https://js.qualified.com'], + 'connect-src': ['wss://*.qualified.com', 'https://app.qualified.com'], + 'frame-src': ['https://app.qualified.com'], + }, simpleAnalytics: { 'script-src': ['https://simple.cloudsmith.com'], 'connect-src': [