TL;DR
CSP is an HTTP header that tells browsers "only run scripts from these trusted sources." It's your defense against XSS attacks where hackers inject malicious JavaScript into your site. Start with Content-Security-Policy-Report-Only to see what breaks, fix inline scripts by adding nonce attributes, whitelist external domains you trust, then enforce with Content-Security-Policy.
Test using Google's CSP Evaluator. Avoid 'unsafe-inline' and 'unsafe-eval' like the plague—they defeat the whole purpose.

Image Source: https://www.writesoftwarewell.com/content-security-policy/
Introduction
CSP (Content Security Policy) is basically a bouncer for your website. It tells the browser, "Hey, only let in scripts, images or fonts from these approved sources."
Why should you care? Because XSS (Cross-Site Scripting) attacks are everywhere, and CSP is one of the best ways to protect your users from them.
What is Content Security Policy?
Think of your website as a nightclub. Without CSP, anyone can walk in and start doing whatever they want. With CSP, you're the bouncer with a guest list—you explicitly tell the browser which scripts, styles, and other resources are allowed to run.
Here's the basic idea: instead of the browser trusting everything by default, you flip the script. You say "only trust content from these specific places, and block everything else."
A Simple Example
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com -
default-src 'self'- "By default, only load stuff from my own domain" -
script-src 'self' https://trusted-cdn.com- "For JavaScript specifically, allow my domain and this one CDN I trust"
If someone tries to inject a <script> tag pointing to evil-hacker-site.com, the browser just... won't run it. Pretty neat, righ
It's Not Just About XSS
CSP also protects against:
- Clickjacking - Someone embedding your site in an iframe to trick users
- Mixed content attacks - Loading HTTP resources on HTTPS pages
- Malicious iframes - Unexpected content being loaded into your page
- Unauthorized data exfiltration - Scripts trying to send data to random domains
Think of it as security insurance. Your input validation might have a bug. Your sanitization library might miss something. CSP is there as a backup.
The Nonce Trick: When You Actually Need Inline Scripts
Okay, so you've got some inline JavaScript that you really need to keep inline. Maybe it's server-rendered data, or a legacy widget that's hard to refactor. What do you do?
Enter the nonce attribute. It's basically a one-time password for your inline scripts.
How Nonces Work
- For every page request, your server generates a random, unguessable string:
const nonce = crypto.randomBytes(16).toString('base64'); // Result: something like "dGhpcyBpcyBhIG5v==" - Add this nonce to your CSP header:
Content-Security-Policy: script-src 'self' 'nonce-dGhpcyBpcyBhIG5v==' - Add the same nonce to your inline script:
<script nonce="dGhpcyBpcyBhIG5v=="> console.log('This inline script is allowed!'); </script> - The browser checks: "Does the nonce in the script match the nonce in the CSP header? Yes? Cool, run it."
Why this works: An attacker can't inject a script with the correct nonce because they don't know what the random value is. It changes with every page load.
Practical Example: Server-Side Rendering
Let's say you're using Node.js with Express and EJS:
app.get('/dashboard', (req, res) => { // Generate a random nonce const nonce = crypto.randomBytes(16).toString('base64'); // Set CSP header with the nonce res.setHeader( 'Content-Security-Policy', `script-src 'self' 'nonce-${nonce}'` ); // Pass the nonce to your template res.render('dashboard', { nonce, userData: {...} }); }); In your EJS template:
<script nonce="<%= nonce %>"> const userData = <%= JSON.stringify(userData) %>; console.log('User data loaded:', userData); </script> The nonce changes on every request, so even if an attacker sees it once, it's useless for their attack.
Important: What Nonces DON'T Protect Against
Nonces are great, but they won't save you from:
- XSS vulnerabilities in your inline code itself
- Scripts loaded from whitelisted domains that are compromised
- JavaScript that's already on the page and gets called with malicious input
They're a way to say "this inline script is mine," not "this inline script is safe."
Key CSP Directives
Here are the most important directives you should know:
-
default-src: Fallback for other directives -
script-src: Controls where scripts can be loaded from -
style-src: Controls stylesheets -
img-src: Controls images -
connect-src: Controls AJAX, WebSocket, and fetch requests -
font-src: Controls fonts -
frame-src: Controls frames and iframes -
frame-ancestors: Controls where your page can be embedded -
base-uri: Controls the<base>tag -
form-action: Controls form submission targets
Common CSP Values
-
'self': Same origin as the document -
'none': Block everything -
'unsafe-inline': Allow inline scripts/styles (avoid if possible!) -
'unsafe-eval': Allow eval() and similar (avoid if possible!) -
https:: Allow any HTTPS resource -
https://example.com: Allow specific domain -
'nonce-{random}': Allow specific inline script with matching nonce -
'sha256-{hash}': Allow specific inline script with matching hash
How to Actually Implement CSP (Without Breaking Everything)
Here's the thing: you can't just slap a strict CSP on your site and call it a day. If you do, everything will break and your users will revolt. Trust me, many developers have learned this the hard way.
Here's the smart approach:
Step 1: Start in Report-Only Mode
Use the Content-Security-Policy-Report-Only header first:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations This is genius because the browser will:
- Not block anything - Your site works normally
- Report violations - You get a JSON report of what would have been blocked
You're basically doing a dry run.
Step 2: Set Up a Violation Report Endpoint
Create an endpoint to catch these reports:
// Example Express.js endpoint app.post('/csp-violations', express.json({ type: 'application/csp-report' }), (req, res) => { console.log('CSP would have blocked:', req.body); // Maybe save to database for analysis res.status(204).end(); }); Or just use a service like report-uri.com that visualizes everything for you.
Step 3: Fix Your Code Based on Reports
You'll probably discover:
- Inline scripts everywhere (especially in legacy code)
- Scripts loaded from CDNs you forgot about
- Third-party widgets that load their own stuff
- Inline event handlers like
onclick="doThing()"
Time to refactor! Move inline scripts to external files, or use nonce attributes (more on that in a sec).
Step 4: Switch to Enforcement Mode
Once the reports stop coming (or you've whitelisted legitimate sources), change from Content-Security-Policy-Report-Only to Content-Security-Policy.
Now you're actually protecting your users!
Step 5: Keep Both Headers Running
Pro tip: Keep report-only mode running even after you enforce. Use it to test stricter policies:
Content-Security-Policy: default-src 'self' Content-Security-Policy-Report-Only: default-src 'none'; script-src 'self' This way you can see what a stricter policy would break before you deploy it.
Testing Your CSP
Using Google's CSP Evaluator
Google provides an excellent tool for testing your CSP: CSP Evaluator
Simply paste your CSP header, and it will:
- Highlight security issues
- Suggest improvements
- Explain potential vulnerabilities
Manual Testing
- Check browser console: CSP violations appear in the console
- Test inline scripts: Try adding an inline script—it should be blocked
- Test external resources: Try loading resources from non-whitelisted domains
- Use browser DevTools: Network tab shows blocked resources
Example Test
<!-- This should be blocked with a proper CSP --> <script>alert('This is an inline script')</script> <!-- This should also be blocked --> <img src="https://untrusted-domain.com/image.jpg"> Common CSP Mistakes (And How to Fix Them)
1. Using 'unsafe-inline' (The Cardinal Sin)
Content-Security-Policy: script-src 'self' 'unsafe-inline' Why this is terrible: This is like installing a security door and then leaving it wide open. It allows any inline script to run, including the ones attackers inject. You just defeated the entire purpose of CSP.
The fix: Use nonce attributes instead (see below) or move your scripts to external files.
2. Using 'unsafe-eval' (Also Bad)
Content-Security-Policy: script-src 'self' 'unsafe-eval' Why this sucks: This allows eval(), new Function(), and similar code execution methods. Attackers love this.
The fix: Refactor your code. Modern JavaScript rarely needs eval(). If you think you need it, you probably don't.
3. Whitelisting Everything with Wildcards
Content-Security-Policy: script-src * What you just did: "Allow scripts from literally anywhere on the internet"
The fix: Be specific. List the exact domains you trust.
4. Forgetting About base-uri
If you don't set base-uri, attackers can inject <base> tags to hijack relative URLs.
The fix: Always include base-uri 'self' or base-uri 'none' in your policy.
5. Whitelisting Entire CDNs
Content-Security-Policy: script-src 'self' https://cdnjs.cloudflare.com The problem: CDNs host thousands of libraries. An attacker can use any of them, including old vulnerable versions of jQuery.
The fix: Use Subresource Integrity (SRI) hashes:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK" crossorigin="anonymous"></script> Now the browser will only run it if the hash matches.
6. Trusting JSONP Endpoints
If you whitelist a domain that has JSONP endpoints, attackers can abuse them to bypass your CSP.
The fix: Don't whitelist domains with JSONP. Use CORS-enabled APIs instead.
Real-World Example: Strict CSP
Here's an example of a strict, production-ready CSP:
Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-{random}'; style-src 'self' 'nonce-{random}'; img-src 'self' https://trusted-images.com; font-src 'self'; connect-src 'self' https://api.example.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests; This policy:
- Blocks everything by default
- Only allows scripts and styles from same origin with nonces
- Restricts images to same origin and a trusted CDN
- Prevents the page from being framed
- Forces HTTPS upgrades
Best Practices
- Start strict, then relax if needed: It's easier to add exceptions than to tighten a loose policy
- Use nonces for inline scripts: Generate a new random nonce for each page load
- Avoid 'unsafe-inline' and 'unsafe-eval': These significantly weaken your CSP
- Monitor violations: Keep report-only mode running alongside enforcement
- Use SRI for third-party resources: Add integrity checks to external scripts and styles
- Keep your policy maintainable: Document why each source is whitelisted
- Test thoroughly: Use automated tests to ensure CSP doesn't break functionality
- Update regularly: As your app evolves, keep your CSP up to date
Beyond XSS: Other Security Benefits
Preventing Clickjacking
Content-Security-Policy: frame-ancestors 'none' This prevents your site from being embedded in iframes, protecting against clickjacking attacks.
Enforcing HTTPS
Content-Security-Policy: upgrade-insecure-requests This automatically upgrades HTTP requests to HTTPS.
Restricting Form Targets
Content-Security-Policy: form-action 'self' This prevents forms from submitting to external domains, which could be used for phishing.
Debugging CSP Issues
When things don't work:
- Check the console: CSP violations are logged with detailed information
- Use Report-Only mode: Test changes without breaking production
- Verify nonce generation: Ensure nonces are unique per request
- Check for typos: CSP syntax is strict
- Test in multiple browsers: Implementation may vary slightly
Wrapping Up
Look, CSP isn't a magic bullet. You still need to validate inputs, escape outputs, and follow other security best practices. But it's one of the most powerful tools in your security toolkit.
Here's what you need to remember:
The Good News:
- CSP blocks most XSS attacks, even if your other defenses fail
- Modern browsers all support it
- It's not that hard to implement if you do it gradually
The Reality Check:
- You can't just flip a switch—you need to refactor your code
- Legacy codebases with inline scripts everywhere will be a pain
- You'll probably break something in production at least once (we all do)
Your Action Plan:
- Start with report-only mode TODAY (seriously, it's free intel)
- Fix the low-hanging fruit (inline event handlers, obvious inline scripts)
- Use nonces for the stuff you can't easily refactor
- Test with Google's CSP Evaluator
- Gradually tighten your policy over time
When You Really Need CSP:
- You handle sensitive data (payments, healthcare, personal info)
- You have user-generated content (comments, forums, profiles)
- You want to sleep better at night
When You Can Probably Skip It:
- Static blog with no user interaction
- Internal tools behind authentication with no user input
- You're just starting out and have bigger fish to fry
Final thought: Start strict and relax if needed. It's way easier than starting loose and trying to tighten things later.
Now go secure your websites! 🔒
Further Reading and Resources
MDN Web Docs - CSP: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
Comprehensive documentation with examples and browser compatibility informationContent Security Policy Reference: https://content-security-policy.com
Quick reference guide with examples and explanations for all CSP directivesGoogle CSP Evaluator: https://csp-evaluator.withgoogle.com/
Essential tool for testing and validating your CSP headersWrite Software Well - CSP Guide: https://www.writesoftwarewell.com/content-security-policy/
Excellent practical guide with real-world examples and implementation strategiesCSP Level 3 Specification: https://www.w3.org/TR/CSP3/
Official W3C specification for the latest CSP featuresOWASP CSP Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
Security-focused best practices from OWASP
Stay secure, and happy coding!
Top comments (0)