forked from colin/resume
2
0
Fork 0

Add utils.js with SHA-256 hash in CSP

This commit is contained in:
Your Name 2025-03-31 05:01:34 -04:00
parent cc0142f000
commit 630ef90df1
3 changed files with 101 additions and 4 deletions

60
docker/resume/utils.js Normal file
View File

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

View File

@ -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();
}
});
});

View File

@ -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(/<script/g, `<script nonce="${res.locals.nonce}"`);
res.send(html);
} else {
next();
}
});
// Serve static files from the docker/resume directory
app.use(express.static(path.join(__dirname, '../docker/resume')));