Cross-site scripting has been a problem for as long as websites have allowed users to input data. Attackers find ways to inject scripts into pages, steal cookies, deface sites, or redirect visitors to malicious places. Traditional defenses focus on filtering and escaping user input. Those approaches work, but they are reactive. You have to catch every possible injection point. Miss one, and you have a problem.
Content Security Policy takes a different approach. Instead of trying to catch bad input, you tell the browser exactly what is allowed on your page. Anything not on the list gets blocked. No exceptions. It changes the game from hunting down every vulnerability to defining clear boundaries for your content.
Think of CSP as giving the browser a set of rules. These rules say where scripts can come from, where images can load, where styles can be applied, and what the page is allowed to do. If an attacker manages to inject a script tag into your page, the browser looks at the CSP rules. If that script does not come from an approved source, the browser refuses to run it. The attack fails.
This is powerful because it works even if your input filtering fails. You do not have to catch every clever trick an attacker might use. You just have to define your boundaries clearly. The browser enforces them automatically. It is defense in depth at its best.
The policy travels in an HTTP header. The browser reads it when it loads your page and applies the rules for that page and any resources it loads. You can also use a meta tag, but the header gives you more control and works for all resources, not just the HTML page.
CSP has many directives, but you do not need to use all of them. Start with the ones that matter most. Default-src is the fallback. If you do not specify a directive for a specific type of resource, the browser uses the default-src rules. Setting a restrictive default-src is a good way to build a secure policy.
Script-src controls where JavaScript can come from. This is the most important directive for preventing XSS. If you only allow scripts from your own domain, any injected script from another domain gets blocked. You can also allow specific third-party domains like your analytics provider or a CDN.
Style-src works the same way for CSS. Image-src controls images. Font-src handles web fonts. Connect-src controls AJAX requests and WebSocket connections. Frame-src and child-src control iframes. Each directive gives you fine-grained control over a specific type of content.
Here is a basic policy to get started:
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'
This policy says block everything by default. Then allow scripts, styles, images, fonts, and AJAX calls only from the same origin. It is a good starting point for many sites. If you use external services, you add their domains to the appropriate directives.
The 'self' keyword means the same origin as the document. If your site is example.com, 'self' allows resources from example.com. It does not allow subdomains or other domains. You can add subdomains explicitly if you need them.
Inline scripts and styles are a common source of problems. CSP blocks them by default unless you specifically allow them. This is intentional. Inline code is harder to control and is often where injection attacks happen. The best approach is to move your JavaScript into external files and your CSS into external stylesheets. This makes your code cleaner and your CSP tighter.
Sometimes you cannot avoid inline code. Maybe you are using a third-party widget that requires it. Or you have generated content that needs inline styles. CSP gives you a few options. You can use the 'unsafe-inline' keyword, but that defeats much of the security benefit. Better to use nonces or hashes.
A nonce is a random value you generate for each request. You include it in your CSP header and also add it to your script tags. The browser matches them. If they match, the inline script runs. If not, it is blocked. This works well for scripts that need to be inline but are generated legitimately by your server.
Here is an example with a nonce:
Content-Security-Policy: script-src 'nonce-abc123'
And in your HTML:
<script nonce="abc123">console.log('this runs');</script>
Hashes work similarly but are better for static inline content. You hash the content of the script, put the hash in your CSP, and the browser compares them. If the content changes, the hash no longer matches and the script gets blocked.
Turning on CSP can break your site if you get the rules wrong. I have seen this happen. Someone sets a strict policy, deploys it, and suddenly half the site stops working. Images do not load. Menus stop responding. Forms do not submit. This is not a good experience for anyone.
That is why CSP has a report-only mode. Instead of using Content-Security-Policy, you use Content-Security-Policy-Report-Only. The browser applies the rules, but instead of blocking violations, it sends reports to a URL you specify. Your site keeps working. You get a list of everything that would have been blocked.
Run in report-only mode for a while. Look at the reports. See what resources are being used that your policy would block. Adjust your policy to include the legitimate ones. Once you are confident, switch to enforcement mode. This approach saves you from breaking your site and gives you a chance to understand your actual resource usage.
Setting up a report endpoint takes some work. You can build your own, but services like Report-URI make it easier. They collect the reports, show them in a dashboard, and help you analyze what is happening. The report endpoint is included in the header like this:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report-endpoint
Every site is different, so your CSP will reflect your specific needs. Let us build a realistic policy step by step. Start with default-src 'self'. This allows everything from your own domain. Then add restrictions as you go.
If you use Google Analytics, you need to allow scripts and images from google-analytics.com. Add them to script-src and img-src. If you use a CDN like Cloudflare or Fastly, add those domains to script-src and style-src. If you load fonts from Google Fonts, add font-src and style-src for fonts.googleapis.com and fonts.gstatic.com.
Here is a more complete example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.google-analytics.com https://code.jquery.com; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' https://www.google-analytics.com; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'
This policy allows your own resources, Google Analytics, jQuery from a CDN, and Google Fonts. It blocks frames and plugins entirely. It restricts the base URL for relative links. It is fairly restrictive but allows the services many sites need.
Beyond the basic directives, CSP has some advanced options that add extra protection. Frame-ancestors controls who can embed your site in an iframe. This is the modern replacement for X-Frame-Options. It prevents clickjacking attacks. Setting frame-ancestors 'none' means no one can frame your site. Setting it to 'self' allows framing only from your own pages.
Base-uri restricts where the base tag can point. An attacker who can inject a base tag can redirect relative links to malicious sites. Restricting base-uri to 'self' prevents this.
Form-action limits where forms can submit. If you set form-action 'self', your forms can only submit to your own domain. This prevents attackers from redirecting form submissions to their own servers.
Upgrade-insecure-requests is another useful directive. It tells the browser to automatically convert HTTP URLs to HTTPS. This can help clean up mixed content issues without changing every URL individually.
One common mistake is using 'unsafe-inline' or 'unsafe-eval' without thinking. These keywords disable important protections. 'unsafe-inline' allows inline scripts and styles. 'unsafe-eval' allows eval and similar functions. Both are useful during development but should be removed or replaced with nonces and hashes in production.
Another mistake is being too permissive. Allowing scripts from any domain with '*' completely defeats the purpose of CSP. An attacker can host their script anywhere and your page will run it. The same goes for 'data:' and 'blob:' URLs, which can be used to inject code. Avoid these unless you have a specific, well-understood need.
I have also seen policies that are so restrictive they break the site. People forget that browser extensions might need to run scripts. They block legitimate third-party services. They forget about web fonts or analytics. The report-only mode helps catch these issues before they cause problems.
Once your policy is in place, keep an eye on it. Check your reports regularly. Look for violations that might indicate real attacks or might just be services you forgot to whitelist. Adjust your policy as your site evolves.
Browser developer tools show CSP violations in the console. You can see exactly what was blocked and why. This is helpful for debugging. If something is not working and you suspect CSP, open the console and look for messages.
There are also online tools that validate your CSP. They check for syntax errors, missing directives, and common mistakes. Use these before deploying a new policy, especially if you are new to CSP.
Sites that implement CSP properly see real security improvements. XSS attacks that would have succeeded are blocked automatically. Attackers looking for easy targets move on to sites without CSP. The peace of mind alone is worth the effort.
I worked with a site that had multiple XSS vulnerabilities. They were struggling to find and fix every issue. We implemented a strict CSP with report-only mode first. The reports showed exactly where scripts were being injected. We fixed the vulnerabilities, but more importantly, even if a new vulnerability appeared later, CSP would block it. The site went from being constantly at risk to being well protected.
Another site was worried about third-party scripts. They used many external services and wanted to make sure those services could not do anything malicious. CSP gave them control. They could allow the scripts they needed while blocking unwanted behavior. They could see exactly what each script was trying to do and restrict it if necessary.
CSP is not a set-it-and-forget-it solution. Your site changes. You add new services. You update your code. Each change might require adjustments to your CSP. Make CSP part of your development process. Test new features with CSP enabled. Update your policy when you add new third-party resources.
If you use a content management system or a framework, check if it has CSP support built in. Some platforms can generate nonces automatically or help manage your policy. This makes implementation easier and more consistent.
Start small. Use report-only mode. Get your policy right. Then enforce it. Over time, you will build a policy that protects your site without getting in your way. And you will have one of the strongest defenses available against injection attacks.