From 630ef90df14e6d739012ba29a0953b199e9245c7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 31 Mar 2025 05:01:34 -0400 Subject: [PATCH] Add utils.js with SHA-256 hash in CSP --- docker/resume/utils.js | 60 ++++++++++++++++++++++++++++++++++++++++++ tests/headers.spec.js | 16 +++++++++-- tests/server.js | 29 ++++++++++++++++++-- 3 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 docker/resume/utils.js diff --git a/docker/resume/utils.js b/docker/resume/utils.js new file mode 100644 index 0000000..dfda99f --- /dev/null +++ b/docker/resume/utils.js @@ -0,0 +1,60 @@ +// Utility functions for the resume website + +/** + * Debounce a function to limit its execution rate + * @param {Function} func - The function to debounce + * @param {number} wait - The number of milliseconds to delay + * @returns {Function} - The debounced function + */ +export function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +/** + * Check if an element is in the viewport + * @param {Element} element - The element to check + * @returns {boolean} - Whether the element is in the viewport + */ +export function isInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} + +/** + * Format a date in a consistent way + * @param {Date|string} date - The date to format + * @returns {string} - The formatted date string + */ +export function formatDate(date) { + if (!date) return ''; + const d = new Date(date); + return d.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long' + }); +} + +/** + * Safely get a value from an object using a path + * @param {Object} obj - The object to traverse + * @param {string} path - The path to the value + * @param {*} defaultValue - The default value if not found + * @returns {*} - The value at the path or the default value + */ +export function get(obj, path, defaultValue = undefined) { + return path.split('.') + .reduce((acc, part) => acc && acc[part], obj) ?? defaultValue; +} \ No newline at end of file diff --git a/tests/headers.spec.js b/tests/headers.spec.js index 1d65b97..a50599a 100644 --- a/tests/headers.spec.js +++ b/tests/headers.spec.js @@ -32,7 +32,7 @@ test.describe('Security Headers Tests', () => { } }); - test('should have correct CSP directives', async ({ page }) => { + test('should have correct CSP directives with nonce and hash', async ({ page }) => { await page.goto('http://localhost:8080'); const response = await page.waitForResponse('http://localhost:8080'); const headers = response.headers(); @@ -40,10 +40,22 @@ test.describe('Security Headers Tests', () => { // Check for essential CSP directives expect(csp).toContain("default-src 'self'"); - expect(csp).toContain("script-src 'self' 'unsafe-inline'"); + expect(csp).toContain("script-src 'self' 'nonce-"); + expect(csp).toContain("'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='"); expect(csp).toContain("style-src 'self' 'unsafe-inline'"); expect(csp).toContain("img-src 'self' data: https: http:"); expect(csp).toContain("font-src 'self'"); expect(csp).toContain("connect-src 'self'"); }); + + test('should have nonce attributes on script tags', async ({ page }) => { + await page.goto('http://localhost:8080'); + + // Check that all script tags have nonce attributes + const scripts = await page.$$('script'); + for (const script of scripts) { + const hasNonce = await script.evaluate(el => el.hasAttribute('nonce')); + expect(hasNonce).toBeTruthy(); + } + }); }); \ No newline at end of file diff --git a/tests/server.js b/tests/server.js index 25f1211..6214c5b 100644 --- a/tests/server.js +++ b/tests/server.js @@ -1,15 +1,25 @@ const express = require('express'); const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs'); const app = express(); const port = 8080; +// Generate a random nonce +function generateNonce() { + return crypto.randomBytes(16).toString('base64'); +} + // Security headers middleware app.use((req, res, next) => { - // Content Security Policy + const nonce = generateNonce(); + res.locals.nonce = nonce; + + // Content Security Policy with nonce and hash res.setHeader( 'Content-Security-Policy', "default-src 'self'; " + - "script-src 'self' 'unsafe-inline'; " + + `script-src 'self' 'nonce-${nonce}' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; ` + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https: http:; " + "font-src 'self'; " + @@ -27,6 +37,21 @@ app.use((req, res, next) => { next(); }); +// Custom middleware to inject nonce into HTML +app.use((req, res, next) => { + if (req.path.endsWith('.html')) { + const filePath = path.join(__dirname, '../docker/resume', req.path); + let html = fs.readFileSync(filePath, 'utf8'); + + // Add nonce to all script tags + html = html.replace(/