From 93c645882e4969db886a7e2d7f92038ecba2e0cd Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 4 Feb 2026 21:57:31 +0200 Subject: [PATCH 1/4] feat(security-headers): extend CSP for third-party integrations Allow reCAPTCHA, GTM inline scripts (hashes), LinkedIn Insight, GA, Hotjar, and Facebook Pixel. Add data: for inline fonts (Swiper). --- .../@apostrophecms/security-headers/index.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 website/modules/@apostrophecms/security-headers/index.js diff --git a/website/modules/@apostrophecms/security-headers/index.js b/website/modules/@apostrophecms/security-headers/index.js new file mode 100644 index 00000000..ee53d1b4 --- /dev/null +++ b/website/modules/@apostrophecms/security-headers/index.js @@ -0,0 +1,49 @@ +/** + * Extends @apostrophecms/security-headers to allow reCAPTCHA. + * reCAPTCHA requires script-src and frame-src to include Google domains. + * Without these, the reCAPTCHA script is blocked by CSP and no token is + * generated, causing "Missing reCAPTCHA token" on form submission. + * + * Also allows data: URIs for font-src so that inline base64 fonts (e.g. from + * Swiper or other libraries) are not blocked by CSP. + * + * Adds script-src hash for inline scripts injected by third-party tools (e.g. + * GTM) that cannot use the request nonce. Also allows Hotjar, Facebook Pixel, + * and LinkedIn Insight (script-src, connect-src, img-src). + */ +module.exports = { + improve: '@apostrophecms/security-headers', + options: { + policies: { + recaptcha: { + 'script-src': '*.google.com *.gstatic.com', + 'frame-src': '*.google.com *.recaptcha.net', + }, + inlineFonts: { + 'font-src': 'data:', + }, + inlineScripts: { + 'script-src': + "'sha256-oTA8qLsJHk9g+M1YNjqx2sHGYh6catTGwk9lmCk8hhs=' " + + "'sha256-ZC4Ihfl+1sv3E25DQh090ITQKwffxiocyA9C1vaePKU=' " + + "'sha256-Q/LPXhHka5/egcP/jMtr5hz7Sxemm+1q7K+bOgaJiMo=' " + + "'sha256-3ZRDhT/4WJSTcMHjSSKp1/doi40daSfiQU6ZD395+DA=' " + + "'sha256-huW3ylgdSqVTZdqsJoCPMlhbzwwaT0HRomNhtq49Beo='", + }, + linkedin: { + 'script-src': 'snap.licdn.com', + 'connect-src': 'HOSTS px.ads.linkedin.com', + 'img-src': 'px.ads.linkedin.com', + }, + googleAnalytics: { + 'connect-src': '*.google-analytics.com *.analytics.google.com', + }, + hotjar: { + 'script-src': 'static.hotjar.com', + }, + facebook: { + 'script-src': 'connect.facebook.net', + }, + }, + }, +}; From 94cb3ae6ce241bb9291c67f6aa37a351fc6a50f5 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 4 Feb 2026 22:18:24 +0200 Subject: [PATCH 2/4] feat(security-headers): add CSP policies for analytics and form integrations --- website/modules/@apostrophecms/security-headers/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/website/modules/@apostrophecms/security-headers/index.js b/website/modules/@apostrophecms/security-headers/index.js index ee53d1b4..eb727107 100644 --- a/website/modules/@apostrophecms/security-headers/index.js +++ b/website/modules/@apostrophecms/security-headers/index.js @@ -23,16 +23,20 @@ module.exports = { 'font-src': 'data:', }, inlineScripts: { + /* eslint-disable no-secrets/no-secrets -- CSP script hashes, not secrets */ 'script-src': "'sha256-oTA8qLsJHk9g+M1YNjqx2sHGYh6catTGwk9lmCk8hhs=' " + "'sha256-ZC4Ihfl+1sv3E25DQh090ITQKwffxiocyA9C1vaePKU=' " + "'sha256-Q/LPXhHka5/egcP/jMtr5hz7Sxemm+1q7K+bOgaJiMo=' " + "'sha256-3ZRDhT/4WJSTcMHjSSKp1/doi40daSfiQU6ZD395+DA=' " + - "'sha256-huW3ylgdSqVTZdqsJoCPMlhbzwwaT0HRomNhtq49Beo='", + "'sha256-huW3ylgdSqVTZdqsJoCPMlhbzwwaT0HRomNhtq49Beo=' " + + "'sha256-zoD9yhjUIP539kmB7swNElD1S9L+cey6RvNjUnEcTU4='", + /* eslint-enable no-secrets/no-secrets */ }, linkedin: { 'script-src': 'snap.licdn.com', - 'connect-src': 'HOSTS px.ads.linkedin.com', + 'connect-src': + "'self' localhost d3qlcaacmmrges.cloudfront.net px.ads.linkedin.com", 'img-src': 'px.ads.linkedin.com', }, googleAnalytics: { From 9c4a4475389816e90a312e6250896288bebd42f0 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 4 Feb 2026 22:39:04 +0200 Subject: [PATCH 3/4] feat(security-headers): extend CSP for third-party integrations --- .../@apostrophecms/security-headers/index.js | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/website/modules/@apostrophecms/security-headers/index.js b/website/modules/@apostrophecms/security-headers/index.js index eb727107..c0a0b892 100644 --- a/website/modules/@apostrophecms/security-headers/index.js +++ b/website/modules/@apostrophecms/security-headers/index.js @@ -1,15 +1,14 @@ +const isProduction = process.env.NODE_ENV === 'production'; + +let localhostPart = ''; +if (!isProduction) { + localhostPart = 'localhost '; +} +const connectSrcHosts = `'self' ${localhostPart}d3qlcaacmmrges.cloudfront.net px.ads.linkedin.com`; + /** - * Extends @apostrophecms/security-headers to allow reCAPTCHA. - * reCAPTCHA requires script-src and frame-src to include Google domains. - * Without these, the reCAPTCHA script is blocked by CSP and no token is - * generated, causing "Missing reCAPTCHA token" on form submission. - * - * Also allows data: URIs for font-src so that inline base64 fonts (e.g. from - * Swiper or other libraries) are not blocked by CSP. - * - * Adds script-src hash for inline scripts injected by third-party tools (e.g. - * GTM) that cannot use the request nonce. Also allows Hotjar, Facebook Pixel, - * and LinkedIn Insight (script-src, connect-src, img-src). + * Extends @apostrophecms/security-headers for reCAPTCHA, GTM, GA, Hotjar, + * Facebook Pixel, and LinkedIn Insight. */ module.exports = { improve: '@apostrophecms/security-headers', @@ -35,18 +34,26 @@ module.exports = { }, linkedin: { 'script-src': 'snap.licdn.com', - 'connect-src': - "'self' localhost d3qlcaacmmrges.cloudfront.net px.ads.linkedin.com", + 'connect-src': connectSrcHosts, 'img-src': 'px.ads.linkedin.com', }, googleAnalytics: { 'connect-src': '*.google-analytics.com *.analytics.google.com', }, hotjar: { - 'script-src': 'static.hotjar.com', + 'script-src': '*.hotjar.com *.hotjar.io', + 'connect-src': + 'https://*.hotjar.com https://*.hotjar.io wss://*.hotjar.com', + 'img-src': + 'https://static.hotjar.com https://script.hotjar.com ' + + 'https://survey-images.hotjar.com', + 'font-src': 'https://script.hotjar.com', + 'style-src': 'https://static.hotjar.com https://script.hotjar.com', }, facebook: { 'script-src': 'connect.facebook.net', + 'connect-src': 'https://www.facebook.com', + 'img-src': 'https://www.facebook.com', }, }, }, From 197752cfe5e07549d7030fbe10248f88bab82dad Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Wed, 4 Feb 2026 22:52:58 +0200 Subject: [PATCH 4/4] fix(security-headers): allow non-default dev ports with localhost:* --- website/modules/@apostrophecms/security-headers/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/modules/@apostrophecms/security-headers/index.js b/website/modules/@apostrophecms/security-headers/index.js index c0a0b892..02b06445 100644 --- a/website/modules/@apostrophecms/security-headers/index.js +++ b/website/modules/@apostrophecms/security-headers/index.js @@ -2,7 +2,7 @@ const isProduction = process.env.NODE_ENV === 'production'; let localhostPart = ''; if (!isProduction) { - localhostPart = 'localhost '; + localhostPart = 'localhost:* '; } const connectSrcHosts = `'self' ${localhostPart}d3qlcaacmmrges.cloudfront.net px.ads.linkedin.com`;