Content Security Policy (CSP): A Practical Implementation Guide
In the modern web, Cross-Site Scripting (XSS) remains one of the most prevalent and dangerous vulnerabilities. While input sanitization and output encoding are essential first lines of defense, they are not foolproof. Content Security Policy (CSP) provides a powerful second layer of security that can stop XSS in its tracks, even if an attacker manages to inject a malicious script into your page. By defining a clear policy of what is allowed to run, you significantly reduce the attack surface of your application.
What is CSP?
CSP is an HTTP response header that tells the browser which sources of content (scripts, styles, images, etc.) are trusted. If a script tries to load from an untrusted domain, or if an inline script is detected that doesn't meet the policy's requirements, the browser will block it and (optionally) report the violation to you. It is a declarative security model that shifts the burden of enforcement from the application logic to the browser itself.
Core Directives
A CSP is made up of directives, each controlling a specific type of resource. Mastering these is key to a fine-grained policy:
default-src: The fallback for other fetch directives. If a specific directive likescript-srcis missing, the browser usesdefault-src.script-src: Controls where scripts can be loaded from and whether inline scripts are allowed. This is the most critical directive for preventing XSS.style-src: Controls where CSS can be loaded from.img-src: Controls image sources.connect-src: Limits the domains you can connect to viafetch,XMLHttpRequest, or WebSockets.frame-ancestors: Prevents your site from being embedded in iframes on other sites, effectively mitigating Clickjacking.base-uri: Restricts the URLs that can be used in a document's<base>element.form-action: Restricts the URLs to which a form can be submitted.
The Problem with 'unsafe-inline'
Many developers start with a policy like script-src 'self' 'unsafe-inline'. While this is easy to implement because it doesn't require changing existing code, 'unsafe-inline' effectively disables the primary protection against XSS. If an attacker can inject a <script> tag via a reflected or stored XSS vulnerability, the browser will execute it because you've told it that inline scripts are okay. To be truly secure, you must move away from 'unsafe-inline'.
Nonces and Hashes: The Secure Way to Inline
If you must use inline scripts or styles (for example, to pass server-side data to your frontend), CSP provides two secure alternatives:
- Nonces: A "number used once." Your server generates a random, cryptographically strong string for every request and includes it in the header:
script-src 'nonce-EDNnf03nceIOfn39fn3e'. You then add the same nonce to your script tags:<script nonce="EDNnf03nceIOfn39fn3e">...</script>. Since an attacker can't predict the nonce, their injected scripts will be blocked. - Hashes: You provide a SHA-256 hash of the script's content in the header:
script-src 'sha256-xyz...'. The browser will only execute inline scripts that match that exact hash. This is great for static scripts that don't change between requests.
Implementing CSP Without Breaking Your Site
Deploying a strict CSP on an existing site can be terrifying. One wrong directive can break your analytics, your fonts, or your entire UI. To mitigate this, use the Content-Security-Policy-Report-Only header. In "Report-Only" mode, the browser will not block anything. Instead, it will send a JSON report to a URL you specify (via the report-uri or the newer report-to directive) whenever a violation occurs. This allows you to monitor your site for a few weeks, identify all legitimate sources, and refine your policy before enforcing it.
CSP Level 3 and 'strict-dynamic'
As the web evolves, so does CSP. CSP Level 3 introduced several powerful features, including the 'strict-dynamic' keyword. This allows a script that has been trusted (via a nonce or hash) to load additional scripts without needing to explicitly whitelist every single dependency. This is a game-changer for modern web applications that rely on complex, nested third-party libraries like Google Maps or social media widgets, as it simplifies the policy while maintaining a high level of security.
Trusted Types: Stopping DOM XSS
Another cutting-edge defense is Trusted Types. While standard CSP blocks where scripts can come from, Trusted Types blocks how scripts are created within your JavaScript code. It prevents the use of dangerous "sink" functions like innerHTML, outerHTML, or eval() unless the data being passed to them has been processed by a trusted policy. By combining CSP with Trusted Types, you can create an almost impenetrable defense against both traditional and DOM-based XSS.
Frame Ancestors vs. X-Frame-Options
For years, X-Frame-Options was the standard way to prevent Clickjacking. However, it is limited (e.g., it can't allow multiple specific domains). The CSP frame-ancestors directive is its modern replacement. It allows you to specify exactly which domains are allowed to embed your site in an iframe. If both are present, frame-ancestors takes precedence in modern browsers.
Upgrade Insecure Requests
The upgrade-insecure-requests directive is a simple but powerful tool for migrating a site to HTTPS. It tells the browser to treat all of the site's insecure URLs (those starting with HTTP) as though they have been replaced with secure URLs (those starting with HTTPS). This helps prevent "Mixed Content" warnings and ensures that all traffic is encrypted without having to manually update every link in your database.
Common Pitfalls
- Overly broad sources: Using
script-src *orhttps:is almost as bad as no CSP at all. - Forgetting
connect-src: If you use an API on a different domain, you must explicitly allow it, or yourfetch()calls will fail. - Missing
'self': If you don't include'self', you might block scripts or images hosted on your own domain. - CSS
url()functions: These are governed byimg-srcorfont-src, not juststyle-src.
The Importance of a "Default-Deny" Stance
The most secure way to build a CSP is to start with a default-src 'none' directive. This sets a "default-deny" policy for every type of resource. You then explicitly add back only what you need. For example, if your site only needs scripts from your own domain and images from a specific CDN, your policy would look like: default-src 'none'; script-src 'self'; img-src https://cdn.example.com; style-src 'self';.
This approach is more work upfront, but it ensures that you have a complete inventory of your site's dependencies. It also protects you against future resource types that might be added to the browser; if a new type of fetch is introduced, it will be blocked by default until you decide to allow it. In security, knowing exactly what is allowed is always safer than trying to list everything that is forbidden.
Tools for Success
Writing a CSP header by hand is error-prone. A single missing semicolon or a misspelled directive can render the entire policy invalid. The CSP Header Builder provides an interactive interface to construct your policy, with explanations for each directive and real-time validation. If you're using complex patterns to match domains or paths within your CSP, the Regex Studio can help you verify your logic. For those looking for a real-world example of a hardened CSP, the very site you are on uses a strict, nonce-based policy to protect your data. You can also use the Curl Studio to inspect the headers of any site and see their CSP in action.
Conclusion
CSP is one of the most effective security headers available today. While it requires careful planning and testing to implement correctly, the protection it offers against XSS and other injection attacks is well worth the effort. By moving away from 'unsafe-inline' and embracing nonces, hashes, and report-only mode, you can significantly harden your application's security posture. Start small, use reporting, and gradually move toward a strict "default-deny" policy to keep your users safe.