Back to HTTP Headers

Content-Security-Policy security response

Tells the browser exactly which sources are allowed to load scripts, styles, images, and other resources — the primary defence against XSS attacks.

What it does

Content-Security-Policy (CSP) tells the browser which origins are allowed to load each type of resource on a page. A script from an unlisted origin? Blocked. An inline <script> tag without a nonce? Blocked. An iframe pointing to a suspicious domain? Blocked.

It's the most powerful browser security header available, and the primary mitigation against Cross-Site Scripting (XSS) attacks. XSS happens when an attacker injects malicious scripts into your page — CSP prevents those scripts from executing even if the injection succeeds.

Syntax

Content-Security-Policy: <directive> <source-list>; <directive> <source-list>

Example — a strict but practical policy:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0m123'; style-src 'self' fonts.googleapis.com; img-src 'self' data: cdn.example.com; font-src fonts.gstatic.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Fetch directives

These control where resources can be loaded from. default-src is the fallback for any directive not explicitly set.

Directive Controls
default-src Fallback for all fetch directives not explicitly set
script-src JavaScript sources (<script>, event handlers, javascript: URLs)
style-src CSS sources (<style>, <link rel="stylesheet">, inline styles)
img-src Image sources (<img>, CSS background-image)
font-src Web font sources (@font-face)
connect-src fetch(), XMLHttpRequest, WebSocket, EventSource
frame-src Sources for <iframe> and <frame>
media-src <audio> and <video> sources
object-src <object>, <embed>, <applet> (always set to 'none')
worker-src Web Worker and ServiceWorker scripts
manifest-src Web app manifests

Document directives

Directive Controls
base-uri Valid URLs for <base> element. Set to 'self' to prevent base tag injection.
sandbox Applies sandboxing restrictions to the page (like <iframe sandbox>).
form-action Valid targets for <form action="..."> submissions.
frame-ancestors Which origins can embed this page in an iframe. Supersedes X-Frame-Options.

Source values

Value Meaning
'none' Block everything
'self' Same origin (scheme + host + port must match)
'unsafe-inline' Allow inline scripts/styles. Defeats most XSS protection — avoid.
'unsafe-eval' Allow eval(), Function(), setTimeout(string). Avoid.
'strict-dynamic' Propagate trust from nonce/hash scripts to dynamically loaded scripts
'nonce-<base64>' Allow specific inline scripts/styles with matching nonce attribute
'sha256-<hash>' Allow specific inline scripts/styles matching this hash
https: Any HTTPS source
data: data: URIs (for images: img-src data:)
https://cdn.example.com Specific origin
*.example.com All subdomains of example.com

Nonces — the right way to allow inline scripts

Instead of 'unsafe-inline', use nonces. A nonce is a random value generated per-request:

Server (PHP/Laravel):

$nonce = base64_encode(random_bytes(16));
// Set header:
// Content-Security-Policy: script-src 'self' 'nonce-{$nonce}'

HTML:

<script nonce="rAnd0m123">
    // This script is allowed
    console.log('loaded');
</script>

The browser only executes <script> tags whose nonce attribute matches the one in the CSP header. Injected scripts from XSS payloads won't have the correct nonce and are blocked.

Important: Generate a fresh nonce on every page request. A static nonce is equivalent to 'unsafe-inline'.

Hashes — for specific static inline scripts

If you have a specific inline script that never changes:

Content-Security-Policy: script-src 'self' 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng='

Compute the hash of the script content (no <script> tags, just the content). The browser computes the same hash and allows it if it matches.

strict-dynamic

'strict-dynamic' allows nonce-trusted scripts to dynamically load additional scripts, without you having to explicitly allowlist each loaded origin:

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

Your main script (trusted via nonce) can now import() or document.createElement('script') other scripts. The loaded scripts inherit trust. This is the modern approach for SPAs that bundle and code-split.

Reporting violations

Add report-uri or report-to to collect violation reports without breaking anything:

Content-Security-Policy: default-src 'self'; report-uri /csp-report
Content-Security-Policy: default-src 'self'; report-to csp-endpoint

Or use Content-Security-Policy-Report-Only to monitor violations in shadow mode before enforcing.

Common mistakes and gotchas

Setting 'unsafe-inline' defeats XSS protection. The most common CSP mistake. If you have script-src 'self' 'unsafe-inline', an attacker can inject <script>alert(1)</script> and it'll run. Use nonces or hashes instead.

Forgetting object-src 'none'. Flash and other plugins loaded via <object> can execute arbitrary code. Always set object-src 'none' — Flash is dead but the vector exists.

Using X-Frame-Options and frame-ancestors inconsistently. frame-ancestors in CSP supersedes X-Frame-Options. Set both for maximum compatibility, but ensure they agree.

Not setting base-uri. Without it, an attacker who can inject a <base> tag can redirect all relative URLs to a malicious origin. Always add base-uri 'self'.

Wildcard schemes without restriction. img-src * or default-src https: are very permissive. Use specific origins where possible.

CSP breaking your own app. The most common reason CSP doesn't get deployed. Use Content-Security-Policy-Report-Only to test your policy first — collect reports, fix violations, then switch to enforcing mode.

Real-world examples

Strict policy for a server-rendered app:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}';
  style-src 'self' fonts.googleapis.com;
  img-src 'self' data: *.cdn.example.com;
  font-src 'self' fonts.gstatic.com;
  connect-src 'self' api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';
  form-action 'self'

Permissive policy while auditing (report only):

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

FAQ

Does CSP replace input sanitisation?

No — CSP is defence in depth, not a replacement. Input sanitisation prevents injection in the first place; CSP limits the damage if injection occurs anyway. You need both. A site with perfect sanitisation and no CSP is still more secure than either alone.

Why does 'unsafe-inline' exist if it's unsafe?

Legacy compatibility. Huge amounts of existing web code use inline scripts and styles. 'unsafe-inline' exists to allow sites to adopt CSP incrementally while still functioning. The right path is to add it temporarily, then replace inline scripts with external files or nonces, then remove it.

What's the difference between frame-src and frame-ancestors?

frame-src controls what your page can embed in iframes (outbound). frame-ancestors controls who can embed your page in their iframes (inbound clickjacking protection). They're opposite directions.

Should I set CSP on API responses?

Usually no — CSP is a browser feature for HTML documents. API responses (JSON, XML) consumed programmatically don't benefit from CSP. Set it on HTML page responses.

Fun fact

CSP was proposed by Mozilla engineer Brandon Sterne in 2009 as a response to the endemic XSS problem on the web. The first implementation landed in Firefox 4 in 2011. Despite being 15+ years old, CSP adoption remains surprisingly low — studies consistently find that fewer than 10% of websites set a meaningful CSP header, and many that do set it still include 'unsafe-inline', negating the main protection. It remains one of the most underutilised security tools available to web developers.