fix: csp headers
All checks were successful
Docker CI / release (push) Successful in 2m52s

This commit is contained in:
Nicola Zambello 2025-08-12 15:45:07 +03:00
parent 6e6948b4fd
commit 6dc56412a3
Signed by: nzambello
GPG key ID: 0A7E9D12831FAAF9
5 changed files with 67 additions and 8 deletions

View file

@ -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

View file

@ -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"

55
generate-csp-hashes.js Normal file
View file

@ -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}! <span role=\"presentation\">\${randomEmoji}</span>\`;`;
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');

View file

@ -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;

View file

@ -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"