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:
- Header 1: Report-To (defines the reporting group and endpoint)
- 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 specifiedContent-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 productionContent-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 (includedata:
if you embed small images as data URLs)Content-Security-Policy: img-src 'self' data: https:
-
connect-src
: network destinations for XHR/fetch/WebSocketContent-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 URLContent-Security-Policy: base-uri 'self'
-
form-action
: where forms may submitContent-Security-Policy: form-action 'self' https://payments.example
-
upgrade-insecure-requests
: ask the browser to upgradehttp://
subresources tohttps://
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) Start in
Report-Only
with a relaxed policyExample 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) 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) 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) 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
*
orunsafe-inline
for scripts long-term - Forgetting
base-uri 'self'
andform-action 'self'
- Leaving old
X-Frame-Options
while usingframe-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.