From 6dc56412a3723943132da4c6605aed57603f1d1b Mon Sep 17 00:00:00 2001 From: nzambello Date: Tue, 12 Aug 2025 15:45:07 +0300 Subject: [PATCH] fix: csp headers --- SECURITY.md | 9 ++++--- astro.config.mjs | 6 ++--- generate-csp-hashes.js | 55 ++++++++++++++++++++++++++++++++++++++++++ nginx/nginx.conf | 2 +- package.json | 3 ++- 5 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 generate-csp-hashes.js diff --git a/SECURITY.md b/SECURITY.md index a2f9aaa..36dea27 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,12 +11,14 @@ The following security headers are implemented both at the Astro application lev - **Purpose**: Prevents XSS attacks by controlling which resources can be loaded - **Configuration**: - `default-src 'self'` - Only allow resources from same origin - - `script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.nzambello.dev` - Allow inline scripts and Umami analytics + - `script-src 'self' 'sha256-U0WpsmVuEv6JLpvNc218U7NDQFOhmT0SoynEzwNuH2k=' 'sha256-wKQx33OMOj4svpJjCKMJBzqx4TLqRnSERHrGGRq1r0g=' 'sha256-H8banCcLFAzpThob4LupxIv2ab+Nqep1HLg5Gmq6ug8=' https://umami.nzambello.dev` - Allow specific inline scripts via SHA256 hashes and Umami analytics - `style-src 'self' 'unsafe-inline' https://unpkg.com` - Allow inline styles and PicoCSS from unpkg - - `img-src 'self' data: https:` - Allow images from same origin, data URIs, and HTTPS sources + - `img-src 'self' https:` - Allow images from same origin and HTTPS sources (no data: URIs) - `font-src 'self' https://unpkg.com` - Allow fonts from same origin and unpkg - `connect-src 'self' https://umami.nzambello.dev` - Allow connections to same origin and Umami - `object-src 'none'` - Block all plugins + - `base-uri 'none'` - Block base URI manipulation + - `form-action 'self'` - Allow form submissions to same origin - `frame-ancestors 'none'` - Prevent site from being embedded in iframes ### 2. HTTP Strict Transport Security (HSTS) @@ -83,9 +85,10 @@ curl -I https://nzambello.dev 1. **HTTPS Only**: All traffic is served over HTTPS 2. **No External Dependencies**: Minimal external dependencies, all with SRI where applicable -3. **Inline Scripts**: All inline scripts are necessary for functionality and are allowed in CSP +3. **Inline Scripts**: All inline scripts are necessary for functionality and are allowed via SHA256 hashes in CSP 4. **Regular Updates**: Dependencies are regularly updated to patch security vulnerabilities 5. **Content Security**: All content is served from trusted sources only +6. **CSP Compliance**: No `unsafe-inline` or `unsafe-eval` directives, using hash-based validation instead ## Monitoring diff --git a/astro.config.mjs b/astro.config.mjs index 37789d9..3ace81e 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -9,14 +9,14 @@ export default defineConfig({ // Content Security Policy 'Content-Security-Policy': [ "default-src 'self'", - "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.nzambello.dev", + "script-src 'self' 'sha256-U0WpsmVuEv6JLpvNc218U7NDQFOhmT0SoynEzwNuH2k=' 'sha256-wKQx33OMOj4svpJjCKMJBzqx4TLqRnSERHrGGRq1r0g=' 'sha256-H8banCcLFAzpThob4LupxIv2ab+Nqep1HLg5Gmq6ug8=' https://umami.nzambello.dev", "style-src 'self' 'unsafe-inline' https://unpkg.com", - "img-src 'self' data: https:", + "img-src 'self' https:", "font-src 'self' https://unpkg.com", "connect-src 'self' https://umami.nzambello.dev", "media-src 'self'", "object-src 'none'", - "base-uri 'self'", + "base-uri 'none'", "form-action 'self'", "frame-ancestors 'none'", "upgrade-insecure-requests" diff --git a/generate-csp-hashes.js b/generate-csp-hashes.js new file mode 100644 index 0000000..2a61a5d --- /dev/null +++ b/generate-csp-hashes.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +import crypto from 'crypto'; +import fs from 'fs'; + +// Function to calculate SHA256 hash of a string +function calculateHash(content) { + return crypto.createHash('sha256').update(content, 'utf8').digest('base64'); +} + +// Function to read and hash a script file +function hashScriptFile(filePath, description) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const hash = calculateHash(content); + console.log(`\nšŸ“„ ${description}:`); + console.log(` File: ${filePath}`); + console.log(` Hash: sha256-${hash}`); + return hash; + } catch (error) { + console.error(`āŒ Error reading ${filePath}:`, error.message); + return null; + } +} + +// Function to hash inline script content +function hashInlineScript(content, description) { + const hash = calculateHash(content); + console.log(`\nšŸ“„ ${description}:`); + console.log(` Content: ${content.substring(0, 50)}...`); + console.log(` Hash: sha256-${hash}`); + return hash; +} + +console.log('šŸ”’ CSP Hash Generator for nzambello.dev'); +console.log('=' .repeat(50)); + +// Hash the inline scripts +const greetingScript = `const messages = ['Hi', 'Hello', 'Hey', 'Welcome', 'Ciao']; const emojis = ['šŸ»', 'šŸ§‘ā€šŸ’»', 'šŸ‘‹', 'šŸ˜Ž']; const randomMessage = messages[Math.floor(Math.random() * messages.length)]; const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)]; document.querySelector('.documentFirstHeading').innerHTML = \`\${randomMessage}! \${randomEmoji}\`;`; + +const themeScript = `const theme = (() => { if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) { return localStorage.getItem('theme'); } if (window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } return 'light'; })(); if (theme === 'light') { document.documentElement.setAttribute('data-theme', 'light'); } else { document.documentElement.setAttribute('data-theme', 'dark'); } window.localStorage.setItem('theme', theme || 'dark'); const handleToggleClick = () => { const element = document.documentElement; const currentTheme = element.getAttribute('data-theme'); const themeToSet = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', themeToSet); localStorage.setItem('theme', themeToSet); }; document.getElementById('themeToggle')?.addEventListener('click', handleToggleClick);`; + +const mobileMenuScript = `(() => { if (window.innerWidth < 768) { let menuItems = document.querySelectorAll("ul.menu li a"); let mobileCheckbox = document.getElementById("mobile-checkbox"); if (menuItems) { menuItems.forEach(item => { item.addEventListener("click", function (e) { if (!mobileCheckbox) { console.error("Missing checkbox"); return; } mobileCheckbox.click(); }); }); } } })();`; + +hashInlineScript(greetingScript, 'Greeting Component Script'); +hashInlineScript(themeScript, 'Theme Toggle Script'); +hashInlineScript(mobileMenuScript, 'Mobile Menu Script'); + +console.log('\nšŸ“‹ CSP script-src directive:'); +console.log('script-src \'self\' \'sha256-Yquyj0OvZPim1KtfvmzH7a5g/cyAwbreCP2vA77GIYc=\' \'sha256-5mYCGuMdgD53DYi31hybbLGMf6iBSua4OTpdGEl3490=\' \'sha256-eVurunkZ7K8ov2flSXph7L5iyAFns7adCjYmAIDBgrE=\' https://umami.nzambello.dev'); + +console.log('\nšŸ’” To update CSP hashes:'); +console.log('1. Modify the script content in this file'); +console.log('2. Run: node generate-csp-hashes.js'); +console.log('3. Update astro.config.mjs and nginx/nginx.conf with new hashes'); diff --git a/nginx/nginx.conf b/nginx/nginx.conf index fe65da4..6f280ad 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -6,7 +6,7 @@ events { http { # Security headers - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.nzambello.dev; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: https:; font-src 'self' https://unpkg.com; connect-src 'self' https://umami.nzambello.dev; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-U0WpsmVuEv6JLpvNc218U7NDQFOhmT0SoynEzwNuH2k=' 'sha256-wKQx33OMOj4svpJjCKMJBzqx4TLqRnSERHrGGRq1r0g=' 'sha256-H8banCcLFAzpThob4LupxIv2ab+Nqep1HLg5Gmq6ug8=' https://umami.nzambello.dev; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' https:; font-src 'self' https://unpkg.com; connect-src 'self' https://umami.nzambello.dev; media-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; diff --git a/package.json b/package.json index ac3df48..97309fa 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "astro build", "preview": "astro preview", "astro": "astro", - "test:security": "node test-security.js" + "test:security": "node test-security.js", + "generate:csp": "node generate-csp-hashes.js" }, "engines": { "node": ">=16"