Content Security Policy is the highest-leverage security header — and the most likely to break your site on first deployment. Report-Only + phased rollout is the safe path.
Prerequisites
- HTTPS deployed (see how to add HSTS safely)
- Access to your web server / CDN to set response headers
- An endpoint to receive CSP violation reports (or a service like Sentry, Report URI, DataDome)
Step 1 — Draft a Starting Policy
For a typical web app:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri /csp-violation-report;
Note the -Report-Only suffix. Nothing is blocked yet.
Directive breakdown:
default-src 'self'— fallback for every fetch directivescript-src— where scripts can come fromstyle-src— where CSS can come from ('unsafe-inline'needed for inline<style>tags)img-src— where images can come from (data:allows data URLs)font-src— where fonts can come fromconnect-src— where fetch / XHR / WebSocket can targetframe-ancestors 'none'— who can embed you in an iframe (replaces X-Frame-Options)base-uri 'self'— restricts<base>tag targetsform-action 'self'— where forms can POSTupgrade-insecure-requests— auto-upgrade any HTTP subresource to HTTPSreport-uri— where violations are sent
Step 2 — Deploy Report-Only
Ship the Content-Security-Policy-Report-Only header to production. Nothing blocked. Violations sent to your endpoint.
Step 3 — Collect Violations (1 Week)
Every embed not in your policy generates a violation report. Common early findings:
- Google Analytics — needs
https://www.google-analytics.cominscript-srcandconnect-src - Google Tag Manager — needs
https://www.googletagmanager.cominscript-src - Stripe checkout — needs
https://js.stripe.cominscript-srcandhttps://api.stripe.cominconnect-src - Legacy jQuery on a CDN — add the CDN’s origin
- Inline
<script>tags — add'nonce-{unique-per-request}'and set the nonce header
For each violation, decide:
- Legitimate embed — add its origin to the appropriate directive
- Old/unused third-party — remove from your codebase
- Malicious injection — investigate; this is what CSP catches
Step 4 — Iterate
Update the Report-Only header. Wait another week. Repeat until violation reports are only from noisy but harmless sources.
Step 5 — Flip to Enforcing
Remove -Report-Only. Now the header enforces:
Content-Security-Policy:
default-src 'self';
...
report-uri /csp-violation-report;
Keep report-uri — even when enforcing, you want visibility into blocked requests.
Step 6 — Verify
Run the security headers checker — confirm the enforcing CSP is deployed and no critical directive is missing.
Test the site manually — every core user flow should work. Watch violation reports for the first 48 hours; roll back if legitimate features break.
Nonces for Inline Scripts
Rather than 'unsafe-inline' in script-src, use per-request nonces:
<script nonce="abc123xyz">...</script>
Content-Security-Policy: script-src 'self' 'nonce-abc123xyz';
Generate a new nonce per response. Modern frameworks (Next.js, Rails, Django) support this natively.
Common Miss
- Deploying enforcing CSP without a Report-Only bake period — site breaks silently
'unsafe-inline'inscript-src— kills the main XSS protection CSP provides- Missing
frame-ancestors— clickjacking still possible even with X-Frame-Options - No
report-uri— flying blind - Different CSP on different routes — attackers hit the routes without CSP
Follow up with the CSP glossary entry and the full HTTP security headers guide.
Related
Check the CSP glossary entry, read the HTTP security headers explainer, and see how to add HSTS safely.