Content Security Policy is your strongest defense against XSS. Learn how to implement web security headers to lock down your site and stop malicious scripts.
During a recent refactor of a legacy dashboard, we realized our frontend was essentially a playground for potential injection attacks. We had third-party scripts scattered across the codebase, and despite our XSS prevention strategies: A guide for modern web developers, we needed a structural defense that didn't rely on perfect code. That’s when we committed to implementing a strict Content Security Policy.
A Content Security Policy (CSP) acts as an allow-list for your browser. Instead of hoping your code is free of vulnerabilities, you tell the browser exactly which domains are allowed to load resources like scripts, styles, and images. If an attacker manages to inject a <script> tag pointing to their malicious domain, the browser simply refuses to execute it.
When I started, I thought I could just drop a "default-src 'self'" header and call it a day. That broke roughly 60% of our production features within minutes. It’s a common mistake; you have to balance security with the realities of modern web development, where your app likely depends on CDNs, analytics, and third-party widgets.
To implement this, you'll need to configure your server (Nginx, Apache, or your Node.js middleware) to send the Content-Security-Policy header. Don't start with a restrictive policy immediately. Instead, use Content-Security-Policy-Report-Only to log violations without breaking your site.
Here is a balanced approach for a standard web application:
| Directive | Purpose |
|---|---|
default-src 'self' | Fallback for all resource types |
script-src 'self' trusted-cdn.com | Restricts scripts to your origin and a specific CDN |
style-src 'self' fonts.googleapis.com | Allows your CSS and Google Fonts |
frame-ancestors 'none' | Prevents clickjacking, as discussed in our Preventing Clickjacking with Modern Frame-Ancestors Policies guide |
script-src 'self' 'unsafe-inline' 'unsafe-eval'. Note that 'unsafe-inline' effectively disables the main benefit of CSP for XSS prevention, but it's a necessary starting point for legacy apps.report-uri or a simple internal logging endpoint to track what's getting blocked.The biggest hurdle is almost always inline scripts. If you have legacy code like <script>doSomething();</script>, a strict CSP will kill it. The modern way to handle this is using a CSP nonce.
When your server renders the page, generate a unique random string (the nonce) for every request. Add it to your header and your script tags:
HTTPContent-Security-Policy: script-src 'nonce-random123'
HTMLstyle="color:#808080"><style="color:#4EC9B0">script nonce="random123"> console.log("This will execute!"); style="color:#808080"></style="color:#4EC9B0">script>
This ensures that only scripts you explicitly authorized for that specific page load can run. If an attacker injects a script, they won't have the current nonce, and the browser will block it.
I learned the hard way that third-party analytics tools are the biggest violators. They often inject their own scripts dynamically, which can be a nightmare to debug under a strict policy. If you're struggling with Preventing DOM-based XSS: A Guide for Modern JavaScript Apps, don't assume a CSP will catch everything—it's a defense-in-depth tool, not a silver bullet.
Also, be wary of the OWASP CSP guide. It’s an incredible resource, but don't copy-paste the most restrictive example if you aren't ready for it. You will break your site. Start small, monitor your reports, and tighten the screws over time.
Does a CSP prevent all types of XSS?
No. A CSP is a mitigation tool. It won't prevent DOM-based XSS if you are using dangerous sinks like eval() or innerHTML with unsanitized user input. You still need to write secure code.
What happens if I don't set a CSP? Your application relies entirely on the developer's ability to sanitize every single input. If there's a single injection point, the attacker can execute arbitrary JavaScript in the user's browser, steal cookies, or redirect them to malicious sites.
Should I use unsafe-inline?
Only as a temporary measure. The goal of a modern CSP is to move away from inline scripts entirely, favoring external files and nonces.
I’m still experimenting with strict-dynamic, which is a newer feature that simplifies managing scripts loaded by other scripts. It’s promising, but browser support can be inconsistent depending on your user base. My advice? Don't wait for the perfect policy. Ship a report-only version today, fix the violations, and harden it next week.
XSS prevention strategies are essential for securing modern web apps. Learn to identify the three main variants and implement robust, layered defenses today.