Back to Articles
Web Security
Updated Sep 9, 2025

Content Security Policy (CSP) Guide

CSP (Content Security Policy) is one of the most effective defenses against cross‑site scripting, but it’s also one of the easiest to misconfigure. This guide gives you working policies, a safe migration path, and framework-specific tips.

Why CSP matters

Cross-site scripting (XSS) lets attackers run scripts in your users’ browsers. CSP restricts where scripts, styles, images, and other resources can load from. Done right, it blocks whole classes of injection-based attacks.

How CSP works

The browser enforces rules (directives) you send in the Content-Security-Policy header on each response. At a high level:

  • You define which sources are allowed for scripts, styles, images, and more
  • For each resource the page tries to load, the user's browser checks it against your directives
  • If a specific directive is missing, default-src acts as the fallback
  • In enforcing mode, disallowed loads are fully blocked, and with Report‑Only mode, the disallowed resources are just reported

You can and should start with Report-Only to measure impact before enforcing.

Report-Only mode: a safe rollout path

Content-Security-Policy-Report-Only lets you see what would be blocked without actually risking to break any pages or functionality. Use it first to find and fix potentially valid violations, then switch to the enforcing header. Reporting is optional. You have two options you can choose between:

No automated reports

You will only see violations in browser's DevTools. Easy to get started, but you will miss seeing violations from other users' browsers. Example:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-<random>' https:

Automated reports

Automated reporting via the browser Reporting API. Requires both of the two headers below:

  1. Header 1: Report-To (defines the reporting group and endpoint)
  2. Header 2: Content-Security-Policy-Report-Only (your CSP configuration)

Examples:

Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}]}
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-<random>' https:; report-to csp-endpoint

Browsers deliver violation reports to the defined endpoint via HTTP POST with JSON (Reporting API). In this example, reports are sent to https://example.com/csp-reports. Reports may be batched. Ensure that the specified API endpoint accepts POST over HTTPS and parses JSON. Also make sure you can view and monitor the data sent to this endpoint, so that you can adjust your CSP or web application accordingly, if needed.

Note: Report-To is not embedded inside the CSP header. It is its own header that defines a named reporting group that your CSP references with report-to <group>. Using only one of these two (Report-To or report-to in CSP) will not produce automated reports.

How to use it effectively:

  • Start with a relaxed Report-Only policy and collect violations for a few days
  • Review and fix violations, such as inline scripts violations (with nonces/hashes), removing unused third‑party sources, and tightening allowlists
  • When violations drop to near zero, switch to an enforcing Content-Security-Policy header

Collecting reports

To collect reports automatically, expose an HTTPS endpoint that accepts POSTed JSON from the browser. Example:

// Minimal Node/Express handler (TypeScript/JavaScript)
import express from 'express'
const app = express()

// Accept common CSP reporting content types
app.use(express.json({ type: [
  'application/csp-report',
  'application/reports+json',
  'application/json'
] }))

app.post('/csp-reports', (req, res) => {
  console.log('CSP report:', JSON.stringify(req.body))
  // TODO: forward to your log pipeline / storage
  res.sendStatus(204) // acknowledged with no content
})

app.listen(3000)

You can test quickly with curl:

curl -X POST https://example.com/csp-reports \
  -H 'Content-Type: application/csp-report' \
  -d '{"csp-report":{"document-uri":"https://example.com","violated-directive":"script-src","blocked-uri":"https://cdn.example"}}'

Example violation payload (truncated):

{
  "csp-report": {
    "document-uri": "https://example.com/checkout",
    "violated-directive": "script-src-elem",
    "blocked-uri": "https://cdn.example.net/app.js",
    "original-policy": "default-src 'self'; script-src 'self' 'nonce-AbC123' https:"
  }
}

Tips:

  • Keep the endpoint lightweight, return 204, and rate‑limit to avoid spikes
  • Reports can include URLs and snippets, so avoid logging sensitive data
  • If you use report-to, expect batched JSON per the Reporting API

Common CSP directives and examples

Directives are the rules the browser enforces for your website, controlling which resources can load and from where. Here are the directives you will use most, with practical examples.

  • default-src: fallback for any resource type not otherwise specified

    Content-Security-Policy: default-src 'self'
    
  • script-src: where scripts may load from. Use nonces or hashes for inline code. Nonce example:

    Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-AbC123' https:
    

    Hash example for a small inline snippet:

    Content-Security-Policy: script-src 'self' 'sha256-x+2i2f8yS6G9m0...' https:
    
  • style-src: where styles may load from. Prefer nonces or hashes for inline styles, try avoid 'unsafe-inline' in production

    Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-AbC123' https:
    

    Hash example for a small inline style block:

    Content-Security-Policy: style-src 'self' 'sha256-x+2i2f8yS6G9m0...'
    
  • img-src: allowed image sources (include data: if you embed small images as data URLs)

    Content-Security-Policy: img-src 'self' data: https:
    
  • connect-src: network destinations for XHR/fetch/WebSocket

    Content-Security-Policy: connect-src 'self' https://api.example.com
    
  • frame-ancestors: who may embed your pages in a frame (modern replacement for X-Frame-Options)

    Content-Security-Policy: frame-ancestors 'self' https://partner.example
    
  • base-uri: where the document can set its base URL

    Content-Security-Policy: base-uri 'self'
    
  • form-action: where forms may submit

    Content-Security-Policy: form-action 'self' https://payments.example
    
  • upgrade-insecure-requests: ask the browser to upgrade http:// subresources to https://

    Content-Security-Policy: upgrade-insecure-requests
    

Complete example header:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<random>' 'strict-dynamic' https:; style-src 'self' 'nonce-<random>' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https://api.example.com https:; frame-ancestors 'self' https://partner.example; base-uri 'self'; form-action 'self' https://payments.example; object-src 'none'; upgrade-insecure-requests

Replace <random> with a per-request nonce and update origins (e.g., API, partner, payments) to match your application. If you do not use trusted loader scripts that import others, you can omit 'strict-dynamic' and explicitly enumerate allowed script sources instead.

A safe migration plan

  1. 1) Start in Report-Only with a relaxed policy

    Example relaxed starting policy (Report-Only):

    Content-Security-Policy-Report-Only: default-src 'self' https: data:; script-src 'self' https:; style-src 'self' https: 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' https: data:; connect-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests
    

    This is intentionally broad to reduce breakage while you collect violations. It will report inline scripts and unexpected third‑party sources without blocking them.

  2. 2) Fix reported violations shown in your browser DevTools (Console) and/or at your reporting endpoint. Start in a local or test environment. Note that different environments often need different allowlists (e.g., API base URLs and CDNs).

  3. 3) Enable automated reporting in staging first. Only enable reporting in production after production-specific violations are addressed and the production reporting endpoint has been verified.

  4. 4) Tighten sources and review violations. When violations are near zero in production with reporting enabled, switch to enforcing mode. Keep reporting on to catch regressions.

Nonces vs hashes: which to choose

Use nonces or/and hashes when you must allow inline scripts or styles to run safely. They let the browser verify that specific inline code is allowed, instead of otherwise resorting to the unsafe-inline CSP directive which should be avoided.

Use a nonce when the server can generate a fresh random value per request and inject it into the header and script tags. This is ideal for frameworks that render server‑side (e.g., Next.js/SSR).

Use a hash when the inline script is static and rarely changes. Recompute the SHA‑256 when you make changes and include it in script-src.

Avoid using unsafe-inline long‑term. It’s fine temporarily while you migrate, but should be avoided when possible.

Implementation

You can set the CSP header in several places depending on your stack: at the web server (e.g., Nginx/Apache), inside your application (middleware/framework code), or via a CDN/cloud provider that manages response headers.

Web server header examples

Nginx (add this in a server or location block):

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-ABC123' 'strict-dynamic' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;

Apache with mod_headers (set header in vhost or .htaccess):

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-ABC123' 'strict-dynamic' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"

Next.js example with nonces

Generate a nonce per request and apply it to your script tags and header.

// middleware.ts (nonce generator example)
import { NextResponse } from 'next/server'

export function middleware(req: Request) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const res = NextResponse.next({ request: { headers: new Headers(req.headers) } })
  res.headers.set('x-nonce', nonce)
  res.headers.set('Content-Security-Policy', `default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https:; style-src 'self' 'unsafe-inline' https:; img-src 'self' data: https:; font-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'`)
  return res
}

Apply the nonce to inline scripts:

// _app or layout, when adding critical inline scripts
<script nonce={nonce} dangerouslySetInnerHTML={{ __html: '/* ... */' }} />

Common mistakes

  • Allowing * or unsafe-inline for scripts long-term
  • Forgetting base-uri 'self' and form-action 'self'
  • Leaving old X-Frame-Options while using frame-ancestors

Advanced tips

  • For third-party analytics, prefer hashed inline snippets or nonces
  • Use subresource integrity (SRI) with external scripts
  • Maintain an allowlist registry per app to avoid drift

Third‑party scripts and tags

  • Prefer official npm packages or self‑hosted files over remote <script src> where possible
  • If you must allow a third‑party domain, scope it narrowly with script-src and consider SRI
  • For tag managers, use a nonce on the loader snippet and restrict which destinations they can load

Embeds and frames

  • Control who can embed your pages with frame-ancestors (e.g., only your domains)
  • Allow iframes you embed using frame-src, listing only the origins you actually need

Conclusion

Roll out CSP gradually in Report-Only, fix violations, then enforce. Pair it with other security headers for improved security.

Run a scan and verify your CSP across pages in the Barrion dashboard. Use Barrion's continuous monitoring to make sure your CSP configuration remains secure. For a quick check of your headers, you can also try the Security Headers Test.

Frequently asked questions

Q: Nonce vs hash: which should I use?

A: Use nonces for server-generated inline scripts that change per request and use hashes for static inline snippets shipped with the app. Avoid unsafe-inline long term.

Q: Is Report-Only necessary?

A: No, but it's recommended. Start with Report-Only to collect violations without breaking pages, fix issues, then switch to enforcement.

Q: Do I still need the X-Frame-Options header?

A: No, you should use frame-ancestors in CSP instead. It supersedes X-Frame-Options and is more flexible.

Trusted by IT Professionals

Organizations rely on Barrion to strengthen their security and stay ahead of emerging cyber threats.
Assess your application security today - results in under a minute.

Barrion logo iconBarrion

Barrion delivers automated security scans and real-time monitoring to keep your applications secure.

Contact Us

Have questions or need assistance? Reach out to our team for support.

© 2025 Barrion - All Rights Reserved.