Implementing Report-Only Mode

Implementing Report-Only Mode

Setting up Report-Only mode requires careful configuration of headers and violation reporting infrastructure:

// Express.js Report-Only Implementation
const express = require('express');
const crypto = require('crypto');

class CSPReportOnly {
  constructor(app) {
    this.app = app;
    this.violations = [];
    this.setupMiddleware();
    this.setupReportingEndpoint();
  }
  
  setupMiddleware() {
    this.app.use((req, res, next) => {
      // Generate nonce for this request
      const nonce = crypto.randomBytes(16).toString('base64');
      res.locals.cspNonce = nonce;
      
      // Build report-only policy
      const policy = this.buildPolicy(nonce);
      res.setHeader('Content-Security-Policy-Report-Only', policy);
      
      // Also set reporting endpoint headers
      res.setHeader('Report-To', JSON.stringify({
        group: 'csp-endpoint',
        max_age: 10886400,
        endpoints: [{
          url: '/api/csp-reports'
        }]
      }));
      
      next();
    });
  }
  
  buildPolicy(nonce) {
    const directives = [
      "default-src 'self'",
      `script-src 'self' 'nonce-${nonce}' https://cdn.jsdelivr.net https://unpkg.com`,
      `style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com`,
      "img-src 'self' data: https:",
      "font-src 'self' https://fonts.gstatic.com",
      "connect-src 'self' https://api.example.com wss://realtime.example.com",
      "frame-ancestors 'self'",
      "base-uri 'self'",
      "form-action 'self'",
      "report-uri /api/csp-reports",
      "report-to csp-endpoint"
    ];
    
    return directives.join('; ');
  }
  
  setupReportingEndpoint() {
    // Traditional report-uri endpoint
    this.app.post('/api/csp-reports', 
      express.json({ type: 'application/csp-report' }), 
      (req, res) => {
        this.handleViolationReport(req.body);
        res.status(204).end();
      }
    );
    
    // Modern Report-To API endpoint
    this.app.post('/api/csp-reports',
      express.json({ type: 'application/reports+json' }),
      (req, res) => {
        req.body.forEach(report => {
          if (report.type === 'csp-violation') {
            this.handleViolationReport(report.body);
          }
        });
        res.status(204).end();
      }
    );
  }
  
  handleViolationReport(report) {
    const violation = report['csp-report'] || report;
    
    // Enrich violation data
    const enrichedViolation = {
      ...violation,
      timestamp: new Date().toISOString(),
      userAgent: violation['user-agent'],
      violationType: this.categorizeViolation(violation)
    };
    
    this.violations.push(enrichedViolation);
    this.analyzeViolation(enrichedViolation);
  }
  
  categorizeViolation(violation) {
    const blockedUri = violation['blocked-uri'] || '';
    const violatedDirective = violation['violated-directive'] || '';
    
    if (blockedUri === 'inline') return 'inline-script';
    if (blockedUri === 'eval') return 'unsafe-eval';
    if (blockedUri.startsWith('data:')) return 'data-uri';
    if (violatedDirective.includes('frame-ancestors')) return 'framing-attempt';
    
    return 'external-resource';
  }
}