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