Implementing Hash-Based CSP
Implementing Hash-Based CSP
Hash-based CSP allows specific inline scripts and styles by their content hash, providing security for static inline content that doesn't change between requests:
// Hash-Based CSP Implementation
class HashBasedCSP {
constructor() {
this.hashes = {
scripts: new Set(),
styles: new Set()
};
}
calculateHash(content, algorithm = 'sha256') {
const hash = crypto.createHash(algorithm);
hash.update(content, 'utf8');
return `'${algorithm}-${hash.digest('base64')}'`;
}
addScriptHash(scriptContent) {
const hash = this.calculateHash(scriptContent);
this.hashes.scripts.add(hash);
return hash;
}
addStyleHash(styleContent) {
const hash = this.calculateHash(styleContent);
this.hashes.styles.add(hash);
return hash;
}
// Scan HTML for inline content and generate hashes
async scanHTMLForInlineContent(htmlPath) {
const html = await fs.promises.readFile(htmlPath, 'utf8');
const cheerio = require('cheerio');
const $ = cheerio.load(html);
// Extract and hash inline scripts
$('script:not([src])').each((_, element) => {
const scriptContent = $(element).html().trim();
if (scriptContent) {
this.addScriptHash(scriptContent);
}
});
// Extract and hash inline styles
$('style').each((_, element) => {
const styleContent = $(element).html().trim();
if (styleContent) {
this.addStyleHash(styleContent);
}
});
// Also check for inline event handlers
const eventHandlers = [
'onclick', 'onload', 'onmouseover', 'onmouseout',
'onkeydown', 'onkeyup', 'onsubmit', 'onchange'
];
eventHandlers.forEach(handler => {
$(`[${handler}]`).each((_, element) => {
const handlerContent = $(element).attr(handler);
if (handlerContent) {
console.warn(`Warning: Inline event handler found (${handler}). Consider refactoring.`);
}
});
});
return {
scriptHashes: Array.from(this.hashes.scripts),
styleHashes: Array.from(this.hashes.styles)
};
}
generateCSPWithHashes() {
const policy = {
'default-src': ["'self'"],
'script-src': ["'self'", ...this.hashes.scripts],
'style-src': ["'self'", ...this.hashes.styles],
'img-src': ["'self'", 'data:', 'https:'],
'font-src': ["'self'"],
'connect-src': ["'self'"]
};
return Object.entries(policy)
.map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
.join('; ');
}
// Webpack plugin for automatic hash generation
createWebpackPlugin() {
return {
apply: (compiler) => {
compiler.hooks.emit.tapAsync('CSPHashPlugin', (compilation, callback) => {
Object.keys(compilation.assets).forEach(filename => {
if (filename.endsWith('.html')) {
const asset = compilation.assets[filename];
const html = asset.source();
// Extract and hash inline content
const modifiedHtml = this.processHTMLWithHashes(html);
// Update asset
compilation.assets[filename] = {
source: () => modifiedHtml,
size: () => modifiedHtml.length
};
}
});
// Generate CSP meta file
const cspPolicy = this.generateCSPWithHashes();
compilation.assets['csp-policy.txt'] = {
source: () => cspPolicy,
size: () => cspPolicy.length
};
callback();
});
}
};
}
}