Why CSP Violations Happen (and Why They’re Worth Fixing)
Content Security Policy (CSP) is one of the most powerful tools we have to prevent cross-site scripting (XSS) and data exfiltration in modern web applications. But right after you enable CSP, you’ll often see a flood of violations in the console and from automated reports. That’s not a failure—it’s the beginning of hardening your application’s behavior.
CSP violations emerge because:
- Third-party scripts (analytics, tag managers, chat widgets) load other resources dynamically.
- Your app uses inline scripts or styles (or
eval-like constructs). - Frameworks add runtime CSS or JavaScript in ways CSP blocks by default.
- Static assets are on CDNs or subdomains not covered by your policy.
- You’re using modern capabilities (Web Workers, WASM, module scripts) without updating directives.
This guide shows you how to systematically diagnose, triage, and fix CSP violations with practical examples and production-ready patterns.
How CSP Works in Practice
CSP is a whitelist-based policy sent via an HTTP header or <meta> tag that defines where resources can load from and which behaviors (like inline code or eval) are allowed. When something violates the policy, the browser blocks it and (optionally) reports it.
- Preferred delivery: HTTP header Content-Security-Policy
- Safe rollout: Content-Security-Policy-Report-Only (monitors without blocking)
- Reports: Reporting API (Report-To) or legacy report-uri endpoint
- Policies are combinative: If multiple policies are sent, the browser enforces all of them. Avoid mixing header and meta unless you know what you’re doing.
Common directives you’ll use:
- default-src: Fallback for resource types not explicitly set
- script-src, style-src: Control scripts and styles; support nonces, hashes, and keywords like 'strict-dynamic' and 'report-sample'
- img-src, font-src, connect-src, media-src, frame-src, worker-src, object-src
- base-uri, frame-ancestors: Limit document base and embedding
- upgrade-insecure-requests, block-all-mixed-content
- require-trusted-types-for 'script' and trusted-types: Build defenses against DOM XSS
Set Up Reporting Before Enforcement
Start in report-only mode to map violations before blocking users.
Example headers:
Content-Security-Policy-Report-Only: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' 'report-sample' https:; style-src 'self' 'nonce-{{RANDOM_NONCE}}' 'report-sample' https:; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https: wss:; worker-src 'self' blob:; frame-src 'self' https:; upgrade-insecure-requests; report-uri /csp-report; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-reports"}],"include_subdomains":true}
Notes:
- Use 'nonce-...' and 'strict-dynamic' on scripts to support secure dynamic loading.
- 'report-sample' helps capture blocked inline snippets for diagnostics (avoid in production on sensitive apps).
- Use both report-to and report-uri for broad browser support.
Example violation report (legacy format):
{
"csp-report": {
"document-uri": "https://shop.example.com/product/123",
"referrer": "",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self'; script-src 'self' 'nonce-abc' 'strict-dynamic'; object-src 'none'; base-uri 'self'",
"blocked-uri": "https://analytics.example-cdn.com/loader.js",
"line-number": 42,
"source-file": "https://shop.example.com/assets/app.js",
"sample": "ga('create', 'UA-XXXXX-Y', 'auto')"
}
}
Reporting API format (modern):
{
"type": "csp-violation",
"age": 0,
"url": "https://app.example.com/",
"user_agent": "Mozilla/5.0 ...",
"body": {
"blockedURL": "inline",
"disposition": "report",
"effectiveDirective": "script-src-attr",
"originalPolicy": "...",
"referrer": "",
"sample": "onclick=doEvil()",
"statusCode": 0
}
}
Build a Minimal Reporting Endpoint
Node/Express example:
import express from 'express';
const app = express();
// Accept both legacy and Reporting API content-types
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/reports+json', 'application/json'] }), (req, res) => {
// Redact potential PII
const report = JSON.stringify(req.body).slice(0, 5000); // guard size
console.log('[CSP]', report);
// TODO: Send to logging pipeline: Cloud Logging, ELK, Sentry, or a data warehouse.
res.status(204).end();
});
app.listen(3000, () => console.log('CSP report endpoint listening on 3000'));
Nginx snippet for adding headers:
add_header Content-Security-Policy-Report-Only "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'nonce-$request_id' 'strict-dynamic' 'report-sample' https:; style-src 'self' 'nonce-$request_id' 'report-sample' https:; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https: wss:; worker-src 'self' blob:; frame-src 'self' https:; upgrade-insecure-requests; report-uri /csp-report; report-to csp-endpoint" always;
add_header Report-To '{"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"https://example.com/csp-report"}],"include_subdomains":true}' always;
Tip: Use a cryptographically random nonce per response, not $request_id in production unless it’s cryptographically strong and unguessable.
A Repeatable Workflow to Triage and Fix Violations
- Enable Report-Only and collect 3–7 days of data.
- Group violations by directive and blocked URL.
- Fix in this order:
- Security-critical policy foundations.
- Inline code (scripts, styles).
- Dynamic loaders (tag managers).
- Third-party domains per directive.
- Advanced cases (workers, WASM, data:).
- Validate in staging with enforcement mode for a subset (canary).
- Roll out gradually; monitor error budgets and conversion impacts.
Foundation: Start With a Safe Baseline
A robust enforced policy looks like:
Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic'; style-src 'self' 'nonce-{{RANDOM_NONCE}}'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' https: wss:; worker-src 'self' blob:; frame-src 'self' https:; upgrade-insecure-requests
Why these choices:
- object-src 'none': Kills old-school plugin attack vectors.
- frame-ancestors 'none': Prevents clickjacking unless you intentionally allow embedding.
- strict-dynamic with nonces: Allows trusted bootstrap scripts to load more scripts without a brittle allowlist.
- Avoid 'unsafe-inline' and 'unsafe-eval' unless absolutely necessary.
Inline Scripts and Styles: Move, Nonce, or Hash
Violations you’ll see:
- "Refused to execute inline script because it violates the following Content Security Policy directive: script-src …"
- "Refused to apply inline style…"
Fix patterns:
- Move inline code into external files hosted on your domain. This is the cleanest approach.
- Use nonces:
- Generate a per-response random nonce server-side.
- Attach nonce to each inline <script> or <style> tag.
- Include 'nonce-...' in script-src and style-src.
Example (Express + templating):
// middleware to set nonce
app.use((req, res, next) => {
res.locals.cspNonce = require('crypto').randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
`script-src 'self' 'nonce-${res.locals.cspNonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${res.locals.cspNonce}'`,
"img-src 'self' data: https:",
"font-src 'self' https:",
"connect-src 'self' https: wss:",
"worker-src 'self' blob:",
"frame-src 'self' https:",
"upgrade-insecure-requests"
].join('; '));
next();
});
In your template:
<script nonce="{{cspNonce}}">
window.appConfig = { env: "prod" };
</script>
<link rel="stylesheet" href="/assets/app.css">
<style nonce="{{cspNonce}}">
/* critical CSS if needed */
</style>
-
Use hashes (for truly static inline snippets):
- Compute a SHA-256 of the exact inline content.
- Include 'sha256-
' in script-src/style-src. - Good for small, immutable snips; bad for anything that changes.
-
Inline event handlers and style attributes:
- Avoid patterns like onclick="", onload="", style="…".
- If you must, consider 'unsafe-hashes' plus hashes of those attributes, but treat this as temporary. Example:
script-src 'self' 'nonce-...' 'strict-dynamic' 'unsafe-hashes' 'sha256-...' - Better: Migrate to addEventListener and CSS classes.
Dynamic Script Loaders and Tag Managers: Use 'strict-dynamic'
Third-party loaders often inject scripts at runtime. Relying on an allowlist of all future domains doesn’t scale.
Solution:
- Add nonces to your initial script tags.
- Include 'strict-dynamic' in script-src.
- Remove broad host allowlists where possible; scripts created by a trusted nonce-bearing script inherit trust.
Example:
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic'
Behavior:
- A script with a valid nonce can programmatically insert new scripts (e.g., analytics, A/B testing), which are allowed without listing every domain.
- Do not combine with wildcards like https: unless necessary. If you do, ensure you still control bootstrap scripts via nonces.
Caveat: Some browsers (older Safari) have limited support for 'strict-dynamic'. Keep minimal domain allowlists if you must support legacy browsers.
Third-Party Domains by Directive: Allow What You Need
- img-src: Frequently needs data: for small inline images; allow your CDN and strict https: for offsite. Avoid using *.
- font-src: If fonts come from a CDN, ensure CORS headers are set (Access-Control-Allow-Origin) and list the domain.
- connect-src: Include APIs, GraphQL endpoints, analytics beacons, Sentry, and WebSocket endpoints (wss:).
- frame-src: Payment iframes, maps, videos. Use exact domains where possible.
- media-src: For audio/video streams.
- worker-src: For Web Workers and Service Workers spawned by pages (note: service worker script itself must comply with CSP on its response).
Example targeted allowances:
img-src 'self' data: https://images.example-cdn.com https://www.google-analytics.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com https://sentry.io https://www.google-analytics.com wss://realtime.example.com;
frame-src 'self' https://player.vimeo.com https://js.stripe.com;
Eval, WASM, and Other Risky Capabilities
Violations:
- "Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source…"
Fix:
- Remove eval, new Function, setTimeout with string arguments, etc.
- For libraries depending on eval (legacy templating, some dev tools), switch to production builds that avoid eval.
- Avoid adding 'unsafe-eval'. If using WebAssembly that relies on eval-like behavior, prefer modern toolchains that don’t need 'wasm-unsafe-eval'. Only add 'wasm-unsafe-eval' if absolutely required and document why.
Data: and Blob: URLs
- Images: Add data: in img-src if you embed base64 images.
- Styles: Avoid data: in style-src.
- Workers: If you create workers from blobs, add blob: to worker-src.
- Media: Use blob: for media streams if needed.
Example:
img-src 'self' data: https:;
worker-src 'self' blob:;
Web Workers and Service Workers
- worker-src controls dedicated and shared workers. If you load workers from blob URIs, specify blob: explicitly.
- Service worker scripts are fetched like normal scripts and must comply with the CSP of the service worker response. Set appropriate CSP on the service worker script response via server headers.
Example:
worker-src 'self' blob:;
Server for service worker:
location = /sw.js {
add_header Content-Security-Policy "script-src 'self'; object-src 'none'";
}
Mixed Content and Upgrades
- upgrade-insecure-requests: Automatically upgrades http:// to https:// for same resources.
- block-all-mixed-content: Blocks mixed content outright (can be disruptive). Often you can rely on upgrade-insecure-requests.
Example:
upgrade-insecure-requests
Real-World Scenario #1: E‑commerce With Analytics, Chat, and A/B Testing
Symptoms:
- Chat widget injects more scripts dynamically.
- Tag manager loads multiple vendor scripts.
- Payment iframe on checkout.
- Product images and fonts from CDN.
Solution outline:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https:;
style-src 'self' 'nonce-{{RANDOM_NONCE}}' https:;
img-src 'self' data: https://cdn.shop-cdn.com https://www.google-analytics.com;
font-src 'self' https://fonts.gstatic.com https://cdn.shop-cdn.com;
connect-src 'self' https://api.shop.com https://www.google-analytics.com https://sentry.io wss://chat.shop.com;
worker-src 'self' blob:;
frame-src 'self' https://js.stripe.com https://player.vimeo.com;
upgrade-insecure-requests;
Why include https: in script-src/style-src here? To support older browsers lacking 'strict-dynamic' and because tag managers can source scripts from rotating hosts. Keep an eye on this and prune over time.
Action items:
- Nonce the initial bootstrap script and inline config.
- Verify chat widget works in enforcement mode.
- Monitor violations and add explicit domain allowances for any holdout browsers.
Real-World Scenario #2: SPA With Module Scripts and Code Splitting
Symptoms:
- Module scripts (
type="module") and dynamicimport()create new requests. - Build tool injects runtime CSS via style tags.
Fix:
- Add nonces to script tags; ensure bundler respects nonce on injected chunks or rely on 'strict-dynamic'.
- For runtime CSS, use nonced <style> or configure your framework to attach nonces.
Example policy:
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
style-src 'self' 'nonce-{{RANDOM_NONCE}}';
connect-src 'self' https://api.example.com;
img-src 'self' data: https:;
font-src 'self' https:;
Framework integration (Next.js example):
- Set nonce in a custom Document:
// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import crypto from 'crypto';
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
const nonce = crypto.randomBytes(16).toString('base64');
return { ...initialProps, nonce };
}
render() {
const { nonce } = this.props;
return (
<Html>
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
export default MyDocument;
- Set the CSP header using the same nonce in getServerSideProps or a custom server.
Real-World Scenario #3: Legacy CMS With Inline Handlers
Symptoms:
- Tons of inline onclick, onsubmit, and inline styles.
- You can’t refactor everything at once.
Interim policy:
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' 'unsafe-hashes' 'sha256-ABCDEFG...' 'sha256-HIJKLMN...';
style-src 'self' 'nonce-{{RANDOM_NONCE}}' 'report-sample';
Steps:
- Identify the most common inline snippets; produce hashes for them.
- Add 'unsafe-hashes' so hashed event handlers are allowed.
- Plan a phased refactor to external scripts and addEventListener.
- Remove 'unsafe-hashes' and hashes as you eliminate inline code.
DevTools and Debugging Tips
- Chrome DevTools Issues tab: Shows CSP violations with links to docs.
- Console messages include directive, resource URL, and often a sample of blocked code if 'report-sample' is enabled.
- Network panel: Verify CSP header presence and correctness on every HTML document (and service worker scripts).
- Remember that meta CSP applies only to that document and can’t relax a header policy; multiple policies are enforced together.
Testing and Validation Tools
- Google CSP Evaluator: Checks policies for common pitfalls (e.g., allowing https: with no nonces).
- Mozilla Observatory / securityheaders.com: Validates header presence and composition.
- Lighthouse: Flags insecure resource loads and mixed content.
- Automated tests: Use Playwright/Puppeteer to capture console messages for CSP violations in CI.
Trusted Types: CSP’s Powerful Companion
Trusted Types mitigates DOM-based XSS by requiring that certain sinks (innerHTML, eval-like APIs) receive only TrustedHTML/TrustedScript objects.
Add:
require-trusted-types-for 'script';
trusted-types default dompurify#s policyName;
Steps:
- Audit places where the app writes to the DOM with raw HTML or creates script URLs.
- Create a sanitizer policy (e.g., with DOMPurify) and gradually enforce.
- Combine with CSP nonces for strong XSS defense-in-depth.
Handling SRI and CORS With CSP
- If you must load third-party scripts/styles, add Subresource Integrity (SRI) and crossorigin attributes:
<script src="https://cdn.example.com/lib.min.js"
integrity="sha384-..." crossorigin="anonymous" nonce="{{cspNonce}}"></script>
- Ensure your CSP allows the CDN in the relevant directive.
- For fonts, set Access-Control-Allow-Origin on the CDN to prevent CORS errors.
Deployment Strategy and Monitoring
- Start with Report-Only in staging, then a small percentage of production (feature flag).
- Define an “error budget” for violations per 1,000 sessions. Investigate spikes.
- Aggregate reports:
- Group by effectiveDirective, blockedURL host, and user agent.
- Track top violators to prioritize fixes.
- Alerting:
- Spike in script-src violations could indicate a new third-party change.
- Sudden frame-ancestors violations may indicate attempted clickjacking or partner embed failures.
Example SQL for grouping (BigQuery-like):
SELECT
body.effectiveDirective AS directive,
REGEXP_EXTRACT(body.blockedURL, r'https?://([^/]+)/?') AS blocked_host,
COUNT(*) AS cnt
FROM `logs.csp_reports`
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY directive, blocked_host
ORDER BY cnt DESC
LIMIT 50;
Common Violations and Quick Fixes Cheat Sheet
- Blocked inline script: Add nonce to script tag and 'nonce-...' to script-src; or move to external file.
- Blocked inline style: Add nonce to style tag or move CSS to a file.
- 'unsafe-eval' needed: Remove eval usage; switch to production build; avoid adding 'unsafe-eval'.
- Third-party analytics blocked: Add domain to script-src (or rely on 'strict-dynamic' via a nonced loader).
- Images blocked: Add CDN to img-src; include data: if you embed base64 icons.
- Fonts blocked/cors: Add CDN to font-src and configure CORS headers.
- WebSocket blocked: Add wss://host to connect-src.
- Iframe blocked: Add domain to frame-src; if you need to be embedded elsewhere, adjust frame-ancestors accordingly.
- Worker blocked: Add blob: or domain to worker-src.
- Mixed content: Add upgrade-insecure-requests and fix remaining http:// references.
Avoid These Pitfalls
- Relying solely on meta CSP for security. Use HTTP headers and keep meta for experiments only.
- Using wildcards (* or https:) without nonces/hashes, which weakens protection.
- Mixing multiple CSPs that unintentionally contradict; remember they’re all enforced.
- Shipping 'unsafe-inline' or 'unsafe-eval' permanently due to time pressure. Treat them as temporary with a removal ticket.
- Forgetting to add the same nonce to server-rendered framework scripts (e.g., NextScript in Next.js).
A Gradual, Sustainable Rollout Plan
- Inventory resources:
- List all scripts, styles, fonts, images, frames, API endpoints.
- Map to directives.
- Implement nonces:
- Wire server to generate nonce per response.
- Add to inline scripts/styles that must remain.
- Switch to Report-Only with a strict policy.
- Triage and fix:
- Inline and dynamic loading first.
- Third-party domains next.
- Advanced cases last.
- Validate with canary enforcement (1–5% of traffic).
- Enforce for all users; keep reporting enabled for at least 30 days.
- Maintain:
- Add CSP checks to CI (CSP Evaluator).
- Monitor reports and update allowlists intentionally.
- Audit third-party changes quarterly.
Putting It All Together: A Production-Ready Policy Template
Use this as a starting point and tailor to your app:
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic';
style-src 'self' 'nonce-{{RANDOM_NONCE}}';
img-src 'self' data: https:;
font-src 'self' https:;
connect-src 'self' https: wss:;
worker-src 'self' blob:;
frame-src 'self' https:;
upgrade-insecure-requests;
report-to csp-endpoint; report-uri /csp-report
Adjust per your third-party needs (analytics, payments, video) and legacy browser support.
Final Thoughts
CSP violations aren’t headaches to suppress—they’re signals guiding you toward a more secure, predictable application. With a structured approach—Report-Only first, nonces and strict-dynamic for scripts, precise directive allowances, and continuous monitoring—you can achieve a strong CSP that survives real-world complexity: tag managers, SPAs, CDNs, workers, and more.
Treat CSP as living configuration. Review it alongside dependencies and third-party services, test it in CI, and monitor it like any other critical guardrail. Your users—and your incident response team—will thank you.