forked from colin/resume
Add utils.js with SHA-256 hash in CSP
This commit is contained in:
parent
cc0142f000
commit
630ef90df1
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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')));
|
||||
|
||||
|
|
Loading…
Reference in New Issue