Critical Headers 8 min read

Content Security Policy (CSP) Complete Guide

Content Security Policy is the most powerful security header you can deploy. A well-written CSP stops entire classes of attacks — XSS, data injection, clickjacking — before they reach your users. This guide covers everything from the basics to production-ready implementation.

What is Content Security Policy?

CSP is an HTTP response header that tells the browser exactly which resources it is allowed to load and execute on your page. If a resource is not explicitly whitelisted, the browser refuses to load it — even if an attacker manages to inject a script tag into your HTML.

Before CSP existed, there was no way for a server to tell the browser "only run scripts from my own domain." Attackers who found an XSS vulnerability could inject any script they liked, and the browser would faithfully execute it. CSP changes that by moving the trust decision to the server.

CSP is delivered as an HTTP response header named Content-Security-Policy. It contains a list of directives separated by semicolons, each controlling a different type of resource.

How XSS Attacks Work (and Why CSP Stops Them)

Cross-Site Scripting (XSS) happens when an attacker manages to inject JavaScript into a page that other users view. The most common path is through user-supplied content — a comment, a search query, a username — that gets rendered in HTML without proper sanitisation.

Once an attacker's script runs in a victim's browser, it has full access to the page's DOM, cookies, and local storage. It can steal session tokens, redirect the user to a phishing page, or silently send form data to an attacker-controlled server. Without CSP, the browser has no way to distinguish the attacker's injected script from your own legitimate code.

With a strong CSP in place, injected scripts are blocked before they execute because they don't come from an approved source. The attack is neutralised at the browser level, independent of whether your server-side sanitisation is perfect.

CSP Directives Explained

A CSP header is built from directives. Each directive controls a specific content type. Here are the most important ones:

DirectiveControlsExample value
default-srcFallback for all types not explicitly listed'self'
script-srcJavaScript files and inline scripts'self' 'nonce-abc123'
style-srcCSS files and inline styles'self' 'unsafe-inline'
img-srcImages, including from data: URIs'self' data: https://cdn.example.com
object-srcPlugins: Flash, Java applets'none'
base-uriWhat URLs <base> can use'self'
form-actionWhere forms can submit data'self'
frame-ancestorsWhich pages can embed this page in an iframe'none'
connect-srcXHR, fetch, WebSocket, EventSource'self' https://api.example.com
font-srcWeb font files'self' https://fonts.gstatic.com

If a directive is not listed, the browser falls back to default-src. If default-src is also missing, the browser applies no restriction for that content type.

The Dangers of unsafe-inline and unsafe-eval

Two CSP keywords destroy most of the protection CSP provides:

Never use these in script-src if you can avoid it: 'unsafe-inline' allows all inline scripts including injected ones. 'unsafe-eval' allows eval(), Function(), and setTimeout with string arguments — all common XSS payloads.

A CSP with script-src 'self' 'unsafe-inline' provides almost no XSS protection because the attacker's injected <script> tag counts as "inline." Many developers add 'unsafe-inline' because they have inline event handlers (onclick="...") or inline <script> blocks they don't want to refactor. The correct solution is nonces.

Nonce-Based CSP: The Right Way

A nonce (number-used-once) is a random value the server generates on every request. It's included in the CSP header and on each legitimate inline script tag. The browser only runs inline scripts that carry the matching nonce — injected scripts don't have it, so they're blocked.

Content-Security-Policy: script-src 'self' 'nonce-r4nd0mV4lue8x'

In your HTML, legitimate inline scripts get the matching nonce attribute:

<script nonce="r4nd0mV4lue8x">
  // this runs — it has the correct nonce
  initApp();
</script>

The nonce must be cryptographically random and change with every page load. If an attacker could predict the nonce, the protection would fail. Most web frameworks provide built-in nonce generation.

report-uri and report-to

CSP can send violation reports to a server endpoint whenever the browser blocks something. This is invaluable for discovering policy mistakes before they affect users:

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

Start with Content-Security-Policy-Report-Only — this logs violations without actually blocking anything, letting you test a new policy in production before enforcing it.

Real-World CSP Examples

Minimal strict policy (single-page app)

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{SERVER_NONCE}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

Policy with external CDN

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net 'nonce-{SERVER_NONCE}';
  style-src 'self' https://fonts.googleapis.com;
  img-src 'self' data: https://images.example.com;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  frame-ancestors 'none';

Common Mistakes

Copy-Paste Starter CSP

Use this as a starting point, then expand based on what resources your app actually needs:

Start in Report-Only mode for at least a week before switching to enforcement. Check your browser console for violations and add any missing sources before going live.
Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-REPLACE_WITH_SERVER_NONCE';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  report-uri /csp-reports

Once you're confident in the policy, change the header name to Content-Security-Policy to start blocking violations instead of only reporting them.

Does your site have a CSP?

WebAudit checks your CSP quality, detects unsafe-inline and unsafe-eval, and gives you a score with specific fix recommendations.

Check your site's security now →