Understanding Nonces in CSP

Understanding Nonces in CSP

Cryptographic nonces (numbers used once) provide a dynamic solution for allowing specific inline scripts and styles while maintaining strong CSP protection. Each nonce is a unique, randomly generated value that must match between the CSP header and the script or style tag, ensuring only authorized inline content executes.

Implementing a robust nonce system:

// Nonce Generation and Management System
const crypto = require('crypto');

class NonceManager {
  constructor(options = {}) {
    this.algorithm = options.algorithm || 'sha256';
    this.nonceLength = options.nonceLength || 16;
    this.nonceCache = new Map();
    this.cacheTimeout = options.cacheTimeout || 300000; // 5 minutes
  }
  
  generateNonce() {
    const nonce = crypto.randomBytes(this.nonceLength).toString('base64');
    const timestamp = Date.now();
    
    // Cache nonce with timestamp for validation
    this.nonceCache.set(nonce, timestamp);
    
    // Clean old nonces
    this.cleanExpiredNonces();
    
    return nonce;
  }
  
  validateNonce(nonce) {
    if (!this.nonceCache.has(nonce)) {
      return false;
    }
    
    const timestamp = this.nonceCache.get(nonce);
    const age = Date.now() - timestamp;
    
    // Nonce expires after timeout
    if (age > this.cacheTimeout) {
      this.nonceCache.delete(nonce);
      return false;
    }
    
    // Each nonce can only be used once
    this.nonceCache.delete(nonce);
    return true;
  }
  
  cleanExpiredNonces() {
    const now = Date.now();
    for (const [nonce, timestamp] of this.nonceCache.entries()) {
      if (now - timestamp > this.cacheTimeout) {
        this.nonceCache.delete(nonce);
      }
    }
  }
  
  // Generate CSP header with nonce
  generateCSPHeader(nonce, additionalDirectives = {}) {
    const defaultDirectives = {
      'default-src': ["'self'"],
      'script-src': ["'self'", `'nonce-${nonce}'`],
      'style-src': ["'self'", `'nonce-${nonce}'`],
      'img-src': ["'self'", 'data:', 'https:'],
      'font-src': ["'self'"],
      'connect-src': ["'self'"],
      'frame-ancestors': ["'none'"],
      'base-uri': ["'self'"],
      'form-action': ["'self'"]
    };
    
    // Merge with additional directives
    const directives = { ...defaultDirectives, ...additionalDirectives };
    
    return Object.entries(directives)
      .map(([key, values]) => `${key} ${values.join(' ')}`)
      .join('; ');
  }
}

// Express middleware for nonce-based CSP
function createNonceMiddleware(nonceManager) {
  return (req, res, next) => {
    // Generate unique nonce for this request
    const nonce = nonceManager.generateNonce();
    
    // Make nonce available to templates
    res.locals.nonce = nonce;
    
    // Set CSP header with nonce
    const cspHeader = nonceManager.generateCSPHeader(nonce);
    res.setHeader('Content-Security-Policy', cspHeader);
    
    // Override render to inject nonce into inline scripts
    const originalRender = res.render;
    res.render = function(view, options, callback) {
      options = options || {};
      options.nonce = nonce;
      
      originalRender.call(this, view, options, (err, html) => {
        if (err) return callback(err);
        
        // Inject nonce into existing inline scripts
        html = html.replace(/<script(?![^>]*\snonce=)/gi, `<script nonce="${nonce}"`);
        html = html.replace(/<style(?![^>]*\snonce=)/gi, `<style nonce="${nonce}"`);
        
        callback(null, html);
      });
    };
    
    next();
  };
}