🔧 Validate Your CSP Policy

Copy a CSP example, deploy it, then verify it works with a real scan.

Verify with headertest.com →

Deploying a CSP policy without testing first is like deploying a database migration without a backup. It might work. It might take down your entire site.

Report-only mode lets you find out what will break before it actually breaks. The browser logs violations but doesn’t block anything. You get all the data, none of the pain.

The Report-Only Header#

It’s literally just a different header name:

C o n t e n t - S e c u r i t y - P o l i c y - R e p o r t - O n l y : d e f a u l t - s r c ' s e l f ' ; s c r i p t - s r c ' s e l f ' ; r e p o r t - u r i / a p i / c s p - r e p o r t

Same syntax. Same directives. Same evaluation. The only difference: the browser reports violations instead of blocking them.

A Strict Starting Policy#

Start with something intentionally strict. You want to catch everything:

C o n d s s i f c f b f r t e c t m o o r a o e e f r y g n n a s r p n a i l - t n m e m o t u p e s - e e - - r - l t - r s c - u a t S t - s c r t a r c - e - s r c - n i t u c s r c ' s c i r u r c s ' r e ' o i r c ' e s c s s n i ' s l e t e t ' s e f l ' o l ' a y s e l ' f s r f s p - e l f ' e s ' e i P l f ' d ; l ; l / o f ' ; a f ' f c l ' ; t ' n ' s i ; a ; o ; p c : n - y e r - h ' e R t ; p e t o p p r o s t r : t ; - O n l y :

This will catch:

  • External scripts (analytics, chat widgets, A/B testing)
  • Inline scripts and styles
  • External fonts and stylesheets
  • Third-party API calls
  • Iframes
  • Form submissions to external URLs

Report Endpoint Setup#

You need somewhere to receive the reports. Here are implementations for common frameworks:

Node.js/Express#

app.post('/api/csp-report',
  express.json({ type: 'application/csp-report' }),
  (req, res) => {
    const report = req.body['csp-report'];
    
    console.error('[CSP Violation]', {
      page: report['document-uri'],
      blocked: report['blocked-uri'],
      directive: report['violated-directive'],
      source: report['source-file'],
      line: report['line-number'],
      column: report['column-number'],
      fullReport: report,
    });
    
    res.status(204).end();
  }
);

Important: CSP reports are sent with Content-Type: application/csp-report. You need to configure Express to parse this content type. That’s what the express.json({ type: 'application/csp-report' }) part does.

PHP#

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $report = json_decode(file_get_contents('php://input'), true);
    $csp = $report['csp-report'] ?? [];
    
    error_log(sprintf(
        '[CSP] page=%s directive=%s blocked=%s source=%s line=%s',
        $csp['document-uri'] ?? '?',
        $csp['violated-directive'] ?? '?',
        $csp['blocked-uri'] ?? '?',
        $csp['source-file'] ?? '?',
        $csp['line-number'] ?? '?'
    ));
    
    http_response_code(204);
    exit;
}

Python/Flask#

from flask import Flask, request
import logging

app = Flask(__name__)
logger = logging.getLogger('csp')

@app.route('/api/csp-report', methods=['POST'])
def csp_report():
    report = request.json.get('csp-report', {})
    logger.warning('CSP: %s blocked %s on %s at %s:%s',
        report.get('violated-directive', '?'),
        report.get('blocked-uri', '?'),
        report.get('document-uri', '?'),
        report.get('source-file', '?'),
        report.get('line-number', '?')
    )
    return '', 204

Laravel#

Route::post('/csp-report', function (Request $request) {
    $report = json_decode($request->getContent(), true);
    $csp = $report['csp-report'] ?? [];
    
    Log::channel('csp')->warning('CSP Violation', [
        'page' => $csp['document-uri'] ?? 'unknown',
        'blocked' => $csp['blocked-uri'] ?? 'unknown',
        'directive' => $csp['violated-directive'] ?? 'unknown',
    ]);
    
    return response('', 204);
});

Nginx (proxy to your endpoint)#

location = /api/csp-report {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Content-Type application/csp-report;
}

Using the Modern Reporting API#

The report-to directive is the newer approach:

Report-To: {
  "group": "csp",
  "max_age": 31536000,
  "endpoints": [{"url": "https://yoursite.com/api/reports"}]
}

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  report-to csp

The Reporting API endpoint receives a different JSON format than report-uri. If you use both (recommended for compatibility), your endpoint needs to handle both formats.

Dual Mode: Report-Only + Enforcement#

Once you’ve analyzed violations and updated your policy, run both simultaneously:

# Report-only: catches violations not covered by enforcement
Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  report-uri /api/csp-report

# Enforcement: the actual policy
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-RANDOM';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  frame-ancestors 'none';
  base-uri 'self'

This is the ideal state. The enforcement header protects against known attacks while report-only catches new violations from code changes, new third-party integrations, or developer mistakes.

Third-Party Report Services#

If you don’t want to build your own endpoint:

  • Report URI (report-uri.com) — Free tier for one domain. Dashboard for analyzing violations.
  • Sentry — If you already use it for error tracking, it can collect CSP reports alongside JavaScript errors.
  • Axiom / Logtail — Log management services that accept CSP reports.

The Deployment Timeline#

Week 1-2: Deploy strict report-only. Expect lots of violations. That’s expected.

Week 3: Categorize violations:

  • Dead integrations (old A/B tests, deprecated analytics) → remove
  • Legitimate third-party scripts → add to policy
  • Inline scripts from your code → move to external files or add nonces
  • Inline styles from your framework → add ‘unsafe-inline’ to style-src

Week 4: Deploy updated policy in report-only mode. Verify violations dropped significantly.

Week 5: Deploy enforcement alongside report-only (dual mode).

Week 6+: Once enforcement is stable for a month, remove report-only. Keep report-uri for ongoing monitoring.

Verify Your CSP#