From 02f1c33b713b928fb47728ea987972000e8f070b Mon Sep 17 00:00:00 2001 From: Leopere Date: Fri, 7 Mar 2025 19:13:41 -0500 Subject: [PATCH] Update Content Security Policy with specific hashes for inline scripts and styles --- collect-all-csp-hashes.js | 140 +++++++++++++++++++ csp-hash-calculator.js | 38 +++++ docker/showerloop/Caddyfile.default.template | 4 +- extract-and-hash-csp.js | 119 ++++++++++++++++ 4 files changed, 299 insertions(+), 2 deletions(-) create mode 100755 collect-all-csp-hashes.js create mode 100755 csp-hash-calculator.js create mode 100755 extract-and-hash-csp.js diff --git a/collect-all-csp-hashes.js b/collect-all-csp-hashes.js new file mode 100755 index 0000000..88bc74b --- /dev/null +++ b/collect-all-csp-hashes.js @@ -0,0 +1,140 @@ +#!/usr/bin/env node + +/** + * CSP Hash Collector + * + * This script scans all HTML files in a directory structure, + * extracts inline scripts and styles, and calculates SHA-256 hashes + * for use in Content Security Policy headers. + * + * Usage: + * node collect-all-csp-hashes.js + * + * Example: + * node collect-all-csp-hashes.js ./docker/showerloop/public + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +// Function to calculate CSP hash +function calculateHash(content) { + const hash = crypto.createHash('sha256').update(content, 'utf8').digest('base64'); + return `'sha256-${hash}'`; +} + +// Function to extract inline scripts from HTML +function extractInlineScripts(html) { + const scriptRegex = /]*)?>([\s\S]*?)<\/script>/gi; + const scripts = []; + let match; + + while ((match = scriptRegex.exec(html)) !== null) { + const scriptContent = match[1].trim(); + if (scriptContent && !scriptContent.includes('src=')) { + scripts.push(scriptContent); + } + } + + return scripts; +} + +// Function to extract inline styles from HTML +function extractInlineStyles(html) { + const styleRegex = /]*)?>([\s\S]*?)<\/style>/gi; + const styles = []; + let match; + + while ((match = styleRegex.exec(html)) !== null) { + const styleContent = match[1].trim(); + if (styleContent) { + styles.push(styleContent); + } + } + + return styles; +} + +// Function to find all HTML files in a directory recursively +function findHtmlFiles(dir, fileList = []) { + const files = fs.readdirSync(dir); + + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + findHtmlFiles(filePath, fileList); + } else if (path.extname(file).toLowerCase() === '.html') { + fileList.push(filePath); + } + }); + + return fileList; +} + +// Main function +function main() { + if (process.argv.length < 3) { + console.error('Error: Please provide a directory path.'); + process.exit(1); + } + + const directoryPath = process.argv[2]; + + try { + console.log(`\nScanning directory: ${directoryPath}`); + const htmlFiles = findHtmlFiles(directoryPath); + console.log(`Found ${htmlFiles.length} HTML files.`); + + const scriptHashes = new Set(); + const styleHashes = new Set(); + let totalScripts = 0; + let totalStyles = 0; + + htmlFiles.forEach(filePath => { + try { + const html = fs.readFileSync(filePath, 'utf8'); + + const scripts = extractInlineScripts(html); + const styles = extractInlineStyles(html); + + totalScripts += scripts.length; + totalStyles += styles.length; + + scripts.forEach(script => { + const hash = calculateHash(script); + scriptHashes.add(hash); + }); + + styles.forEach(style => { + const hash = calculateHash(style); + styleHashes.add(hash); + }); + } catch (err) { + console.error(`Error processing file ${filePath}: ${err.message}`); + } + }); + + console.log(`\nProcessed ${totalScripts} inline scripts and ${totalStyles} inline styles.`); + console.log(`Found ${scriptHashes.size} unique script hashes and ${styleHashes.size} unique style hashes.`); + + console.log('\n=== Complete CSP Directives ==='); + + console.log('\nScript CSP directive:'); + console.log(`script-src 'self' blob: ${Array.from(scriptHashes).join(' ')}`); + + console.log('\nStyle CSP directive:'); + console.log(`style-src 'self' 'unsafe-inline' ${Array.from(styleHashes).join(' ')}`); + + console.log('\nComplete CSP header:'); + console.log(`Content-Security-Policy: default-src 'self'; script-src 'self' blob: ${Array.from(scriptHashes).join(' ')}; style-src 'self' ${Array.from(styleHashes).join(' ')}; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; worker-src 'self' blob:"`); + + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/csp-hash-calculator.js b/csp-hash-calculator.js new file mode 100755 index 0000000..1312532 --- /dev/null +++ b/csp-hash-calculator.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +/** + * CSP Hash Calculator + * + * This script calculates SHA-256 hashes for inline scripts and styles + * to be used in Content Security Policy headers. + * + * Usage: + * node csp-hash-calculator.js "" + * + * Example: + * node csp-hash-calculator.js "document.addEventListener('DOMContentLoaded', function() { console.log('Hello'); });" + */ + +const crypto = require('crypto'); + +// Function to calculate CSP hash +function calculateHash(content) { + const hash = crypto.createHash('sha256').update(content, 'utf8').digest('base64'); + return `'sha256-${hash}'`; +} + +// Get content from command line argument or stdin +const content = process.argv[2] || ''; + +if (!content) { + console.error('Error: No content provided. Please provide content as a command line argument.'); + process.exit(1); +} + +// Calculate and display the hash +const hash = calculateHash(content); +console.log('CSP Hash:'); +console.log(hash); +console.log('\nAdd to your CSP header:'); +console.log('For script: script-src ' + hash); +console.log('For style: style-src ' + hash); \ No newline at end of file diff --git a/docker/showerloop/Caddyfile.default.template b/docker/showerloop/Caddyfile.default.template index 079599e..4b43adb 100644 --- a/docker/showerloop/Caddyfile.default.template +++ b/docker/showerloop/Caddyfile.default.template @@ -48,8 +48,8 @@ # Frame Options (prevents clickjacking) X-Frame-Options "SAMEORIGIN" - # Update CSP to allow media content, scripts, and blob URLs - Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; worker-src 'self' blob:" + # Update CSP to allow media content, scripts, and blob URLs with hashes + Content-Security-Policy "default-src 'self'; script-src 'self' blob: 'sha256-ahmIukdQI/ax+3q3Lrjh0Nuqv1/WxkBIGp+vaji6n8w=' 'sha256-qXRIcycfl2OwZL/s1bd83TFw2OcgcMsv1efLB/P3oOs=' 'sha256-SBn8uB66KTUeApEMuYlK6vZG0XcFpXLKlsbXswTKang=' 'sha256-/nvt7GhhWJsKGTVATnlAsNH54uy+pwbcjfx9Z9CT/u0=' 'sha256-rEjWap8xDw9tc9ULmaSD7VQycQasVvSd1OUiH9xKMTM=' 'sha256-9YtzahjQAT4luPVKC0lfwKhhBxWtN3zkQm99EHsc1bk=' 'sha256-PdtHVmWDPYQUs6SFGLloIwo3P4rG5A7ACmYWE1W4Gmk=' 'sha256-ALpx63KUUcf6ky/Teq3GLd+LlD+t+TpXN+bv/1++prU=' 'sha256-llDQiboC1dyoUHsUebHmXSwCs/k0znV6kWogS1Govvs=' 'sha256-zhuCqwglnTqPZ3YumUUbXlmgy3fN4NGHmK+wQzsoQic=' 'sha256-aCakwry3g1c1frt10sPVerFht/3JKT8i7ij3Aoxtsqw=' 'sha256-WE9M5TeJ2Xj1O9eh+0bg7XLyucO5+HCMccMznmiyocw=' 'sha256-FcjCj8HX/odDguAR0bldjsSdXOQMOLnCBKvlLHMZPZI=' 'sha256-tz6nsCI6ZDRK9g0tLDGMU5j9DBRx74XOe8xqaag7D3E=' 'sha256-IsinOLsxFzlWG2kdQIgMjg7l2ebbAaMbWWNSComW7EE=' 'sha256-p92qjinn1HJIBQCKu3QBxLsdKRh4NTdjvCax1ifSpw4=' 'sha256-17JNXqVQbWEbcxlPw9O3wCCa8PEFW9lwv6rOxRzkmXI=' 'sha256-uRkRZZ6nSw2qypQ46ShF3X/DRaPwWezfixlC4pkDuwo=' 'sha256-7bYe3kxYZPs9D4vqScBDsNEjqOw+n8pUFwyFObBKIjw=' 'sha256-IQIGMyVnkPj80HHZ8/Z8ZyxRC5ZPSFiGtTKsUdDqqOs='; style-src 'self' 'sha256-BBl1Pb4QBQZyj2HmRgFr/OhuPRYwV0zoE6G+08FM5TM=' 'sha256-DPggA6+WHJsxuaWoYLnB8XoTcBjKTnq+AmEhXZ2wJfw=' 'sha256-VyDqCue31iv/ickZ+WUp5RF3wMLAGo01mUL0VdbSTc8=' 'sha256-0ZDDv9ptap3zxZW4gGFrmDP9Y5osppDLJj0gRhecFN8=' 'sha256-c9m3RGxNzIy6ShTOIsmAgY77OyuTfgYCG3B2secjHc4=' 'sha256-rweYv4ZmpQ37GLZ2aJrWCpv486xCBOtOb6ngN4dBn8s=' 'sha256-dE50whpmj5sYr02WC5zh9QQNj6tVUQz1eTMmzJh6OU8=' 'sha256-3av5Wckr9yfHOVSXT8j0+EhuI9xI0Jld43e2jilZsro='; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; worker-src 'self' blob:" # Remove Server header -Server diff --git a/extract-and-hash-csp.js b/extract-and-hash-csp.js new file mode 100755 index 0000000..93caaab --- /dev/null +++ b/extract-and-hash-csp.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node + +/** + * CSP Hash Extractor and Calculator + * + * This script extracts inline scripts and styles from HTML files + * and calculates SHA-256 hashes for use in Content Security Policy headers. + * + * Usage: + * node extract-and-hash-csp.js + * + * Example: + * node extract-and-hash-csp.js ./docker/showerloop/public/index.html + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +// Function to calculate CSP hash +function calculateHash(content) { + const hash = crypto.createHash('sha256').update(content, 'utf8').digest('base64'); + return `'sha256-${hash}'`; +} + +// Function to extract inline scripts from HTML +function extractInlineScripts(html) { + const scriptRegex = /]*)?>([\s\S]*?)<\/script>/gi; + const scripts = []; + let match; + + while ((match = scriptRegex.exec(html)) !== null) { + const scriptContent = match[1].trim(); + if (scriptContent && !scriptContent.includes('src=')) { + scripts.push(scriptContent); + } + } + + return scripts; +} + +// Function to extract inline styles from HTML +function extractInlineStyles(html) { + const styleRegex = /]*)?>([\s\S]*?)<\/style>/gi; + const styles = []; + let match; + + while ((match = styleRegex.exec(html)) !== null) { + const styleContent = match[1].trim(); + if (styleContent) { + styles.push(styleContent); + } + } + + return styles; +} + +// Main function +function main() { + if (process.argv.length < 3) { + console.error('Error: Please provide an HTML file path.'); + process.exit(1); + } + + const filePath = process.argv[2]; + + try { + const html = fs.readFileSync(filePath, 'utf8'); + + const scripts = extractInlineScripts(html); + const styles = extractInlineStyles(html); + + console.log(`\nAnalyzing file: ${filePath}`); + console.log('\n=== Inline Scripts ==='); + + if (scripts.length === 0) { + console.log('No inline scripts found.'); + } else { + console.log(`Found ${scripts.length} inline scripts.`); + const scriptHashes = new Set(); + + scripts.forEach((script, index) => { + const hash = calculateHash(script); + scriptHashes.add(hash); + console.log(`\nScript #${index + 1}:`); + console.log(`${script.substring(0, 100)}${script.length > 100 ? '...' : ''}`); + console.log(`Hash: ${hash}`); + }); + + console.log('\nScript CSP directive:'); + console.log(`script-src 'self' ${Array.from(scriptHashes).join(' ')}`); + } + + console.log('\n=== Inline Styles ==='); + + if (styles.length === 0) { + console.log('No inline styles found.'); + } else { + console.log(`Found ${styles.length} inline styles.`); + const styleHashes = new Set(); + + styles.forEach((style, index) => { + const hash = calculateHash(style); + styleHashes.add(hash); + console.log(`\nStyle #${index + 1}:`); + console.log(`${style.substring(0, 100)}${style.length > 100 ? '...' : ''}`); + console.log(`Hash: ${hash}`); + }); + + console.log('\nStyle CSP directive:'); + console.log(`style-src 'self' ${Array.from(styleHashes).join(' ')}`); + } + } catch (err) { + console.error(`Error: ${err.message}`); + process.exit(1); + } +} + +main(); \ No newline at end of file