Content Security Policy: Protecting Your Website from Injection Attacks
Master Content Security Policy headers to defend against XSS and injection attacks. Learn CSP directives, nonces, hashes, SRI, and incremental deployment strategies.
Content Security Policy: Protecting Your Website from Injection Attacks
Cross-site scripting, universally abbreviated as XSS, has ranked among the most dangerous web application vulnerabilities for over two decades. The attack is conceptually simple: an attacker injects malicious JavaScript into a web page, and when an unsuspecting user visits that page, the injected script executes in the user's browser with the full privileges of the legitimate site. The consequences range from stolen session cookies and credential harvesting to complete account takeover, financial fraud, and data exfiltration. Despite widespread awareness, XSS vulnerabilities remain pervasive because the web's fundamental architecture — mixing code and data in HTML documents, loading scripts from multiple origins, and executing user-generated content — creates an enormous attack surface that is difficult to secure through input validation alone.
Content Security Policy, commonly known as CSP, was invented to address this fundamental problem. Rather than relying solely on developers to correctly sanitize every piece of user input — a task that has proven historically unreliable — CSP provides a declarative security layer that tells the browser exactly which sources of content are legitimate. If an attacker manages to inject a script tag or an inline event handler into the page, the browser blocks it because it does not match the policy. CSP does not replace input validation, but it provides a powerful defense-in-depth mechanism that catches the attacks that slip through.
The Origins of CSP
CSP was first proposed by Mozilla in 2004 and implemented experimentally in Firefox under the header name X-Content-Security-Policy. The concept was refined through collaboration with the broader web standards community, and the first official specification, CSP Level 1, became a W3C Candidate Recommendation in 2012. CSP Level 2, published in 2016, added significant features including nonce-based and hash-based source expressions. CSP Level 3, the current working draft, introduces additional capabilities like strict-dynamic that make CSP more practical for complex modern web applications. Today, CSP is supported by all major browsers and is considered a baseline security requirement for any serious web application.
How CSP Headers Work
CSP is delivered to the browser via an HTTP response header. When the browser receives a page with a Content-Security-Policy header, it parses the policy and enforces it for the duration of that page's lifecycle. The policy consists of a series of directives, each specifying a content type and a list of approved sources. If the page attempts to load a resource that violates any directive, the browser blocks the request and optionally reports the violation to a specified endpoint.
The header itself is a single string containing semicolon-separated directives. A simple policy might look like this: Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'. This policy says that by default, resources should only be loaded from the same origin as the page. Scripts may additionally be loaded from a specific CDN. Styles may be inline — a concession to practicality that we will discuss shortly. Every resource type not explicitly covered falls back to the default-src directive.
CSP can also be set via a <meta> tag in the HTML document, though the header approach is preferred because the meta tag cannot set all directives (notably frame-ancestors and reporting directives), and because it can be bypassed if an attacker can inject content before the meta tag is parsed.
Directive Types
CSP provides a rich set of directives that control different categories of content. The default-src directive serves as the fallback for any content type that does not have its own directive. It is common practice to set default-src 'none' and then explicitly whitelist each content type, following the principle of least privilege.
The script-src directive is arguably the most important, as it controls which scripts can execute on the page. Getting this directive right is the primary defense against XSS. The style-src directive controls stylesheets and inline styles. The img-src directive controls image sources, which is relevant for preventing data exfiltration via image requests to attacker-controlled servers. The connect-src directive controls which origins can be contacted via XHR, Fetch, WebSocket, and EventSource, making it critical for API security. The font-src directive controls web font loading. The frame-src (or its predecessor child-src) controls which origins can be embedded in iframes.
Additional directives include media-src for audio and video, object-src for plugins like Flash (largely obsolete but still important to restrict), base-uri to prevent base tag injection attacks, form-action to control where forms can submit data, and frame-ancestors to control which sites can embed your page in an iframe (the CSP equivalent of the older X-Frame-Options header). The CSP Builder tool helps you construct these policies interactively, letting you define directives and source expressions through a visual interface rather than hand-crafting the header string.
Source Expressions
Each directive takes a list of source expressions that define what is allowed. The keyword 'self' (including the single quotes, which are part of the syntax) allows resources from the same origin as the document. The keyword 'none' blocks all resources of that type. Specific origins can be whitelisted by URL, with support for wildcards in subdomains: https://*.example.com allows any subdomain of example.com over HTTPS.
The most controversial source expressions are 'unsafe-inline' and 'unsafe-eval'. The 'unsafe-inline' keyword allows inline scripts and styles — the very things CSP was designed to block. Many sites resort to 'unsafe-inline' for style-src because CSS-in-JS libraries and framework-generated inline styles are common, but using it for script-src effectively disables CSP's XSS protection. The 'unsafe-eval' keyword allows eval(), Function(), and similar dynamic code evaluation, which is another common XSS vector.
To allow specific inline scripts without opening the door to all inline scripts, CSP Level 2 introduced nonces and hashes. A nonce is a random, single-use token generated by the server for each page load. The server includes the nonce in both the CSP header (script-src 'nonce-abc123') and the script tag (<script nonce="abc123">). Because the attacker cannot predict the nonce, they cannot inject a script that the browser will accept. Hashes work similarly but use the content of the script itself: the server computes a SHA-256, SHA-384, or SHA-512 hash of the inline script's content and includes it in the policy (script-src 'sha256-...'). The browser computes the hash of each inline script and only executes those that match. Hashes are useful for static inline scripts that do not change between page loads.
CSP Reporting
One of CSP's most valuable features is its reporting capability. The report-uri directive (deprecated but still widely used) or the newer report-to directive specifies an endpoint where the browser should send JSON reports when it blocks a resource that violates the policy. These reports include the violated directive, the blocked URI, the document URI, and the source file and line number of the violation. This telemetry is invaluable for debugging policies and for detecting attempted attacks.
The Content-Security-Policy-Report-Only header is perhaps the most important tool for deploying CSP incrementally. This header tells the browser to evaluate the policy and report violations but not actually block anything. It allows you to deploy a candidate policy to production, collect violation reports, and refine the policy until it is tight enough to enforce without breaking legitimate functionality. This report-only testing phase is essential for complex applications with many third-party integrations, where a single misconfigured directive can break critical functionality.
Subresource Integrity
Closely related to CSP is Subresource Integrity, or SRI. While CSP controls which origins can serve resources, SRI verifies that the resource delivered by a permitted origin has not been tampered with. When you include an SRI hash on a <script> or <link> tag, the browser computes the hash of the downloaded file and compares it to the expected hash. If they do not match, the browser refuses to execute or apply the resource.
SRI is essential for any site that loads scripts from third-party CDNs. Even if the CDN is in your CSP whitelist, a compromised CDN could serve malicious code. SRI ensures that even in this scenario, the browser will reject the tampered file. The hash is computed over the exact file contents, so any modification — even a single byte — produces a completely different hash. The SRI Hash Generator makes it easy to compute the correct hashes for your external resources, supporting SHA-256, SHA-384, and SHA-512 algorithms and producing the correctly formatted integrity attribute ready to paste into your HTML.
The combination of CSP and SRI creates a comprehensive content verification system: CSP restricts where resources can come from, and SRI verifies that the resources from those approved origins have not been modified.
Common CSP Mistakes
Deploying CSP incorrectly can create a false sense of security while providing little actual protection. The most common mistake is using overly permissive source expressions. A policy with script-src 'self' 'unsafe-inline' 'unsafe-eval' is barely better than no CSP at all, because attackers can exploit inline scripts and eval to execute arbitrary code. Similarly, whitelisting broad CDN domains like https://cdn.jsdelivr.net can be exploited by attackers who upload malicious packages to those public CDNs — a technique known as a "CSP bypass via CDN."
Another common mistake is forgetting to set object-src 'none'. Flash and Java plugins are nearly extinct, but the object and embed elements can still be used for certain attacks if not restricted. Failing to set base-uri 'self' leaves the site vulnerable to base tag injection, where an attacker injects a <base> tag that redirects relative URLs to an attacker-controlled server. Omitting form-action allows forms to be redirected to external phishing pages.
On the other side, overly restrictive policies can break legitimate functionality in subtle ways. A missing connect-src directive might block API calls that only occur during specific user flows, causing failures that are difficult to reproduce in testing. Font loading, analytics scripts, payment processors, and embedded content from trusted partners all need to be accounted for in the policy. This is why the report-only deployment strategy is so important — it catches these edge cases before they affect users.
Incremental Deployment Strategy
The most successful CSP deployments follow an incremental approach. Start by deploying a report-only policy that approximates your intended restrictions. Use a reporting endpoint to collect violation data for a period of days or weeks, analyzing the reports to identify legitimate resources that need to be whitelisted and potential attacks that confirm the value of the policy. Refine the policy iteratively, tightening restrictions and adding specific allowances as needed.
Once the report-only policy is stable and produces minimal false positives, promote it to an enforcing policy while keeping a report-only policy with even tighter restrictions as the next iteration. This continuous tightening approach avoids the big-bang deployment that often results in broken functionality and emergency policy rollbacks. Many organizations maintain their CSP as code, versioning it alongside their application code and updating it as part of the regular deployment process.
CSP Level 3 and Modern Features
CSP Level 3 introduces features designed to make strict policies more practical for modern web applications. The strict-dynamic source expression is particularly significant. When present in script-src, it tells the browser to trust scripts that are loaded by already-trusted scripts, regardless of their origin. This means that if a nonced script dynamically creates another script element and appends it to the DOM, the dynamically created script will be allowed to execute. This makes nonce-based CSP compatible with common JavaScript patterns like dynamic module loading and script-injected analytics, without requiring every dynamically loaded script's origin to be whitelisted.
The unsafe-hashes source expression allows specific inline event handlers (like onclick attributes) to be permitted by hash, without opening the door to all inline scripts via unsafe-inline. This is a pragmatic concession for legacy applications that use inline event handlers extensively and cannot easily be refactored.
Building a robust Content Security Policy is not a one-time task but an ongoing practice that evolves with your application. As you add new features, integrate new third-party services, or refactor your front-end architecture, the CSP must be updated to reflect the new reality. Tools like the CSP Builder and the SRI Hash Generator make this process more efficient, allowing you to construct valid policies and compute integrity hashes without memorizing the syntax of every directive and algorithm. Combined with a disciplined deployment process using report-only headers and systematic violation analysis, CSP transforms from a daunting security requirement into a manageable, powerful layer of defense that significantly reduces the risk of injection attacks reaching your users.
Related Tools
Related Articles
Try Our Free Tools
200+ browser-based tools for developers and creators. No uploads, complete privacy.
Explore All Tools