Home/Blog/Content Security Policy (CSP): Implementation Guide for 2025
Web Security

Content Security Policy (CSP): Implementation Guide for 2025

Master Content Security Policy implementation with nonce-based and hash-based approaches, learn to prevent XSS attacks, and discover modern CSP best practices for maximum security.

By Inventive HQ Team
Content Security Policy (CSP): Implementation Guide for 2025

Understanding Content Security Policy

Content Security Policy (CSP) is the most powerful security header available to web developers, capable of preventing cross-site scripting (XSS) attacks—one of the most common and dangerous web vulnerabilities. By controlling which resources browsers can load and execute, CSP creates a whitelist-based security model that blocks malicious code even when attackers find injection vulnerabilities.

In 2025, implementing CSP is no longer optional for security-conscious organizations. Modern best practices emphasize "Strict CSP" using nonces or hashes rather than legacy approaches that rely on domain whitelisting. Understanding these implementation strategies is essential for effective XSS prevention.

How CSP Prevents XSS Attacks

XSS attacks work by injecting malicious JavaScript into websites:

Stored XSS: Attacker saves malicious script to database (via comment form, profile field, etc.) Reflected XSS: Attacker includes script in URL parameters that get echoed on page DOM-based XSS: Client-side JavaScript writes untrusted data to dangerous sinks

Without CSP, browsers execute these injected scripts, allowing attackers to:

  • Steal session cookies and credentials
  • Perform actions as the victim user
  • Deface websites
  • Redirect to phishing sites
  • Install keyloggers

CSP prevents execution by establishing which scripts are legitimate and blocking everything else.

CSP Directive Syntax

CSP uses directives to control different resource types:

Content-Security-Policy: directive-name source-expression; another-directive source-expression

Key Directives

default-src: Fallback for other fetch directives script-src: Controls JavaScript sources style-src: Controls CSS sources img-src: Controls image sources font-src: Controls font sources connect-src: Controls XMLHttpRequest, WebSocket, EventSource frame-src: Controls iframe sources frame-ancestors: Controls where the page can be embedded (replaces X-Frame-Options) base-uri: Restricts URLs in <base> element form-action: Controls form submission targets

Source Expressions

'none': Block all sources 'self': Allow same origin only https://example.com: Allow specific domain 'unsafe-inline': Allow inline scripts/styles (dangerous, avoid) 'unsafe-eval': Allow eval() and similar (dangerous, avoid) 'nonce-random123': Allow resources with matching nonce attribute 'sha256-hash...': Allow resources matching cryptographic hash 'strict-dynamic': Trust scripts loaded by already-trusted scripts

Legacy CSP: Domain Whitelisting (Avoid)

The original CSP approach whitelisted trusted domains:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com https://analytics.example.com

Why This Approach Fails

CDN compromise: If whitelisted CDN is compromised, attackers can serve malicious code JSONP endpoints: Many whitelisted domains offer JSONP endpoints that bypass CSP Maintenance burden: Constantly updating domain list as services change 'unsafe-inline' temptation: Developers often add 'unsafe-inline' to avoid refactoring, negating protection

Research shows domain-based CSP is routinely bypassed and provides limited real protection.

Strict CSP: Nonce-Based Approach (Recommended)

Modern "Strict CSP" uses nonces (numbers used once) to identify legitimate scripts:

How Nonces Work

  1. Server generates cryptographically random nonce for each request
  2. Includes nonce in CSP header: script-src 'nonce-random123'
  3. Adds same nonce to legitimate script tags: <script nonce="random123">
  4. Browser executes only scripts with matching nonce
  5. Injected scripts without valid nonce are blocked

Example Implementation

Server generates nonce:

const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');

Sets CSP header:

Content-Security-Policy: script-src 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'

Includes nonce in HTML:

<script nonce="r4nd0m">
  // Legitimate inline script
  console.log('This executes');
</script>

<script src="/app.js" nonce="r4nd0m"></script>

Injected script blocked:

<script>
  // Attacker's injected script - no nonce, blocked!
  alert('hacked');
</script>

Critical Nonce Requirements

Must be cryptographically random: Use secure random generators, not predictable values Must be unique per request: Different nonce for every page load Minimum 128 bits entropy: 16+ bytes for adequate randomness Never reuse: Nonces lose all security if predictable or reused

Strict CSP: Hash-Based Approach

For static sites or specific inline scripts, hash-based CSP allows scripts matching cryptographic hashes:

How Hashes Work

  1. Calculate SHA-256/SHA-384/SHA-512 hash of inline script content
  2. Include hash in CSP: script-src 'sha256-hash...'
  3. Browser hashes actual script content and compares
  4. Matching hashes allow execution; mismatches block

Example

Inline script:

<script>
  console.log('Hello world');
</script>

Generate hash (using openssl):

echo -n "console.log('Hello world');" | openssl dgst -sha256 -binary | openssl base64

Include in CSP:

Content-Security-Policy: script-src 'sha256-Gt1DvTn4uMT8DT6QLQTYdY+1T8TYQ0t5GE8vIVv6pjQ='

Browser allows this specific script because hash matches.

Hash Limitations

Any change breaks hash: Even adding whitespace changes the hash Manual maintenance: Must recalculate hashes when scripts change Build process integration: Typically automated in build pipelines Best for static content: Dynamic content better served by nonces

strict-dynamic: Trusting Script Dependencies

The 'strict-dynamic' directive trusts scripts loaded by already-trusted scripts:

Content-Security-Policy: script-src 'nonce-random123' 'strict-dynamic'; object-src 'none'; base-uri 'none'

How It Works

Trusted script with nonce:

<script nonce="random123" src="/loader.js"></script>

loader.js dynamically creates script (normally blocked):

const script = document.createElement('script');
script.src = '/dynamic-module.js';
document.body.appendChild(script);

With 'strict-dynamic', the dynamically-created script is trusted because it was created by a trusted script (loader.js with valid nonce).

Benefits

Supports modern JavaScript patterns: Module loaders, code splitting, dynamic imports Reduces CSP maintenance: Don't need nonces on every dynamically-loaded script Backward compatibility: Falls back gracefully in older browsers

CSP Report-Only Mode

Before enforcing CSP, test with report-only mode:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Browsers monitor violations and send reports without blocking resources. This allows:

  • Identifying legitimate resources that would be blocked
  • Monitoring for injection attempts
  • Gradually strengthening policy without breaking functionality

Transition to enforcing mode after confirming policy doesn't break legitimate features.

Complete Strict CSP Example

Recommended CSP for modern applications:

Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  require-trusted-types-for 'script';
  report-uri /csp-violations

This policy:

  • Uses nonces for script trust (generated per request)
  • Enables strict-dynamic for script dependencies
  • Blocks plugins (Flash, Java, etc.) with object-src 'none'
  • Prevents base tag injection with base-uri 'none'
  • Requires Trusted Types (additional XSS protection)
  • Reports violations to /csp-violations endpoint

Common Implementation Challenges

Inline Event Handlers

Problem: CSP blocks inline event handlers:

<button onclick="doSomething()">Click</button>

Solution: Move to addEventListener in external/nonce-tagged script:

<button id="myButton">Click</button>
<script nonce="random123">
  document.getElementById('myButton').addEventListener('click', doSomething);
</script>

Third-Party Scripts

Problem: Analytics, ads, widgets need script execution

Solution: Load third-party scripts with nonce or use strict-dynamic to allow them to load their dependencies

Style Inline Attributes

Problem: CSP can block inline style attributes

Solution: Use external stylesheets or hash-based style-src

Testing CSP

Validate CSP implementation:

Browser Console: Check for CSP violation messages CSP Evaluator: Google's CSP Evaluator tool Security Headers Analyzer: Our tool Penetration Testing: Attempt XSS payloads to verify blocking

Conclusion

Content Security Policy is the most effective defense against XSS attacks. Modern Strict CSP using nonces or hashes provides robust protection that legacy domain-whitelisting approaches cannot match.

Implement nonce-based CSP for dynamic applications, hash-based for static content. Use 'strict-dynamic' to support modern JavaScript patterns. Start with report-only mode to identify issues before enforcement.

Proper CSP implementation requires initial effort but provides ongoing protection against one of the web's most dangerous vulnerability classes.

Check your CSP implementation with our Security Headers Analyzer for detailed grading and recommendations.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.