September 10, 2026
Content Security Policy (CSP): Your Browser’s Bodyguard Against XSS Attacks
If you’ve ever built a website, you’ve probably heard of XSS (Cross-Site Scripting) — one of the oldest and most dangerous web…

By Anubhav_bora
5 min read
If you've ever built a website, you've probably heard of XSS (Cross-Site Scripting) — one of the oldest and most dangerous web vulnerabilities. And if you've researched how to defend against it, you've definitely run into CSP (Content Security Policy).
In this post, we'll break down what CSP is, how XSS attacks actually work, and how CSP acts as a safety net when your other defenses fail. We'll go through real examples, and cover important directives like report-uri, report-to, and nonce.
No jargon overload. Just clear, practical explanations.
1. What is XSS (Cross-Site Scripting)?
XSS is an attack where a hacker injects malicious JavaScript into a website, and that script runs in the browser of an innocent victim — often stealing cookies, session tokens, or personal data.
A Simple Example
Imagine a comment box on your blog that doesn't sanitize user input. A normal visitor just types a comment like "Great post!" and it gets displayed as plain text.
But if the site doesn't sanitize input, an attacker can submit a comment that contains a script instead of plain text — one that quietly reads the page's cookies and sends them off to a server the attacker controls.
If your server just stores and displays this comment as-is, every visitor who views that comment page runs the attacker's script — silently sending their session cookie to the attacker's server.
Types of XSS
- Stored XSS — Malicious script is saved in the database (like the comment example above) and served to every visitor.
- Reflected XSS — Script is embedded in a URL or request and reflected back in the response (e.g., a malicious search query link sent via email).
- DOM-based XSS — The vulnerability exists purely in client-side JavaScript, without the server ever seeing the payload.
Why Sanitization Alone Isn't Enough
Developers try to prevent XSS by:
- Escaping special characters used to define markup
- Using safe templating engines
- Validating input
These are essential — but they're not foolproof. Bugs happen, third-party libraries introduce flaws, and one missed edge case can open the door. This is exactly where CSP comes in — as a second layer of defense.
2. What is CSP (Content Security Policy)?
CSP is an HTTP response header that tells the browser: "Only load resources (scripts, styles, images, etc.) from these trusted sources — and block everything else."
Even if an attacker manages to inject a script into your page, CSP can stop the browser from executing it, because the script didn't come from an allowed source.
How CSP is Set
CSP is typically added as an HTTP response header:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.comContent-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.comIt can also be set through a page-level meta declaration instead of a header, which is a common option for static sites that can't easily control server response headers — though the header method is more flexible and secure overall.
Breaking Down the Policy
default-src 'self'→ By default, only load resources from your own domain.script-src 'self' https://cdn.trusted.com→ Scripts can only load from your domain or that specific CDN.
If our earlier attacker tries to inject a script that reaches out to their own server, with a strict CSP in place, the browser will refuse to execute this inline script and log a violation in the console — the attack fails even though the malicious code made it onto the page.
3. Common CSP Directives
default-src— Fallback for all resource typesscript-src— Where JavaScript can load fromstyle-src— Where CSS can load fromimg-src— Where images can load fromconnect-src— Allowed targets for fetch/XHR/WebSocketframe-src— Allowed sources for iframesobject-src— Controls embedded plugin content like Flash/Java applets (usually set to'none')base-uri— Restricts what the page's base URL can be set toform-action— Restricts where forms can submit to
A solid baseline policy often looks like:
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';4. Inline Scripts: The Tricky Part
By default, CSP blocks all inline scripts and styles — including scripts written directly in the page markup and inline event-handler attributes. This is actually one of CSP's biggest wins against XSS, since most XSS payloads rely on inline script injection.
But real-world apps often need inline scripts (analytics snippets, small bits of dynamic JS). You have two safer options instead of just allowing 'unsafe-inline' (which defeats the purpose of CSP):
Option A: Nonces
A nonce ("number used once") is a random, unique value generated by the server on every page load. You add it both to the CSP header and to the specific script element you trust, as an attribute on that element.
Server generates:
Content-Security-Policy: script-src 'self' 'nonce-r4nd0mAbC123';Content-Security-Policy: script-src 'self' 'nonce-r4nd0mAbC123';The matching nonce value is then attached to the one script block you trust on that page. Since the value is random and regenerated on every request, an attacker can't guess it — so any script they inject without the correct nonce gets blocked by the browser, even though the correct script executes normally.
This is one of the most effective ways to allow specific inline scripts while still blocking injected ones.
Option B: Hashes
Instead of a nonce, you can allow a script by its exact SHA hash:
Content-Security-Policy: script-src 'self' 'sha256-abc123examplehash=='Content-Security-Policy: script-src 'self' 'sha256-abc123examplehash=='This only works for static, unchanging inline scripts, since any edit changes the hash.
5. Reporting: report-uri and report-to
Here's a lesser-known but powerful CSP feature: you don't have to only block violations — you can get notified about them too.
This is huge for catching real attacks in progress, debugging your policy, or rolling out CSP gradually without breaking your site.
report-uri (older, still widely supported)
Content-Security-Policy: default-src 'self'; report-uri https://yourapp.com/csp-violation-reportContent-Security-Policy: default-src 'self'; report-uri https://yourapp.com/csp-violation-reportWhenever the browser blocks something due to CSP, it sends a JSON report to that endpoint describing what got blocked — which directive was violated, the URL of the blocked resource, and which page and line the violation happened on.
This tells you exactly what got blocked, from where, and on which page — extremely useful for spotting active XSS attempts in the wild.
report-to (newer standard, replacing report-uri)
report-to works with the broader Reporting API and requires a bit more setup — you define a reporting group via a Report-To header, then reference that group name in your CSP:
Report-To: group="csp-endpoint", max_age=10886400, endpoint="https://yourapp.com/csp-violation-report"
Content-Security-Policy: default-src 'self'; report-to csp-endpointReport-To: group="csp-endpoint", max_age=10886400, endpoint="https://yourapp.com/csp-violation-report"
Content-Security-Policy: default-src 'self'; report-to csp-endpointReport-Only Mode (Test Before You Enforce)
Not sure if a strict CSP will break your site? Use Content-Security-Policy-Report-Only. It reports violations without actually blocking anything:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://yourapp.com/csp-violation-reportContent-Security-Policy-Report-Only: default-src 'self'; report-uri https://yourapp.com/csp-violation-reportThis is the recommended way to roll out CSP in production — watch the reports for a while, fix legitimate issues, then switch to full enforcement.
6. Putting It All Together: A Realistic Policy
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0mAbC123' https://cdn.trusted.com;
style-src 'self' 'nonce-r4nd0mAbC123';
img-src 'self' data: https://images.trusted.com;
connect-src 'self' https://api.yourapp.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-uri https://yourapp.com/csp-violation-report;Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0mAbC123' https://cdn.trusted.com;
style-src 'self' 'nonce-r4nd0mAbC123';
img-src 'self' data: https://images.trusted.com;
connect-src 'self' https://api.yourapp.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-uri https://yourapp.com/csp-violation-report;What this policy does:
- Loads everything from your own domain by default.
- Allows scripts and styles only from your domain, a trusted CDN, or with the correct nonce.
- Blocks embedded plugin content (Flash-like elements) entirely.
- Prevents clickjacking by disallowing your site from being framed elsewhere.
- Sends violation reports to your endpoint so you can monitor for attacks or bugs.
7. CSP Is a Safety Net, Not a Substitute
It's worth being direct about this: CSP does not replace input sanitization, output encoding, or secure coding practices. It's a defense-in-depth layer — the seatbelt, not the reason you drive carefully.
A good security posture looks like:
- Sanitize and encode all user input/output (your first line of defense).
- Use safe frameworks that auto-escape by default (React, Vue, etc., when used correctly).
- Add CSP as a backstop, so even if something slips through, the browser refuses to execute it.
- Monitor violation reports to catch both attacks and misconfigurations early.
Final Thoughts
XSS attacks succeed when malicious script manages to execute in a victim's browser. CSP fights back by giving the browser a strict, explicit list of what's allowed to run — and blocking everything else by default.
Combined with nonces for safely allowing specific inline scripts, and reporting mechanisms like report-uri/report-to to catch violations in real time, CSP turns your browser from a passive executor of any code it receives into an active gatekeeper.
It won't fix bad code. But it will often be the difference between an attacker's script silently succeeding — and it failing loudly, with you getting notified about it.