Even a static website can be abused by attackers in ways that may harm its users. That is why we should do everything reasonably possible to reduce the risks associated with executing untrusted code and the misuse of our website. One tool that can help us achieve this is Content Security Policy (CSP).
What Exactly Is CSP?
Content Security Policy (CSP) is a set of instructions sent to the browser that defines which resources a page is allowed to load and execute.
Different directives can be used to specify which sources are accepted for images, stylesheets, scripts, and other resources. For example:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self' https://cdn.trusted.com;
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
default-src 'self';– fallback policy for resource types that are not explicitly definedscript-src 'self';– allowed JavaScript sourcesstyle-src 'self';– allowed stylesheet sourcesimg-src 'self' data:;– allowed image sourcesfont-src 'self' https://cdn.trusted.com;– allowed font sourcesconnect-src 'self';– controls which endpoints JavaScript can communicate with using Fetch, XMLHttpRequest, WebSocket, or EventSourceobject-src 'none';– controls sources for<embed>,<object>, and<applet>contentbase-uri 'self';– restricts the document’s base URLform-action 'self';– restricts form submission targetsframe-ancestors 'none';– controls which sites may embed the pageupgrade-insecure-requests;– automatically upgrades HTTP requests to HTTPS
These rules are typically delivered through an HTTP response header when we have access to the web server configuration. A common example is configuring CSP in Nginx.
However, CSP can also be defined through a <meta> tag when working with a static site and we do not control the server configuration. It is important to note that some directives (such as frame-ancestors) are not supported when CSP is delivered via a meta tag.
How Can You Check Which CSP Rules a Website Uses?
- Open the browser’s Developer Tools and navigate to the Network tab.
- Select the main document request and inspect the Response Headers section for a
Content-Security-Policyheader. - If no CSP header is present, check the page source for a CSP
<meta>tag.
Hashes and Nonces
If we need inline scripts or styles but still want to prevent unauthorized code execution, there are two common approaches available.
-
If the code is static and does not depend on dynamic data, we can use a hash. The hash value is specified in the CSP policy, and the browser compares it against the hash of the inline script or style. No additional attribute is required on the script tag itself. This allows only the approved inline code to execute. The downside is that even a single-character change requires the hash to be regenerated. Fortunately, many modern tools automate this process. For example, Astro can generate the required hashes automatically when CSP support is enabled.
-
A nonce (“number used once”) is a unique value generated by the server for each page load. The same nonce must be included both in the CSP header and in the
nonceattribute of the corresponding script or style tag. This approach is useful when the content is dynamic and cannot be hashed ahead of time. Because the nonce must be generated at request time, this solution requires server-side processing and is generally not suitable for purely static sites served directly from a CDN.
Modern CSP configurations typically favor nonce- or hash-based approaches because they provide stronger protection against XSS attacks than simple domain allowlists.
What Types of Attacks Can CSP Help Mitigate?
Covering every possible attack vector would be difficult, but let’s look at some common examples.
XSS (Cross-Site Scripting)
XSS occurs when malicious code is injected into a website and executed in the context of another user’s browser. There are several variants of this attack, but they all aim to execute unauthorized code on behalf of the victim.
Supply Chain Compromise
A dependency may become compromised and introduce malicious code into your application. With a hash- or nonce-based CSP, an unexpected modification to a script may no longer satisfy the CSP requirements, causing the browser to block its execution.
Markdown Content Injection
Some Markdown renderers allow raw HTML. If this content is not properly sanitized, it can introduce XSS vulnerabilities.
User Input Compromise
Malicious code may be stored in a database if user input is not properly validated or sanitized. For example, a commenter could submit a malicious script, and the application might later render it for every visitor. In such cases, CSP serves as an additional layer of defense, but it should never be considered a replacement for proper input validation and output encoding.
Clickjacking
Clickjacking occurs when an attacker embeds a legitimate page inside an invisible frame and tricks users into interacting with it. The victim may believe they are clicking a harmless button, while in reality they are approving a transaction or performing another unintended action.
The frame-ancestors directive helps protect against this attack. If you do not intend your site to be embedded in frames at all, setting it to 'none' is generally a good choice.
Because frame-ancestors is not supported in meta-tag CSP, it must be delivered through an HTTP header. Alternatively, the X-Frame-Options header can be used, for example through a _headers configuration file.
Base Tag Hijacking
If an attacker manages to modify the href attribute of the document’s <base> tag, all relative links on the page may be redirected to a malicious destination. The base-uri 'self' directive helps prevent this.
Form Action Hijacking
An attacker may attempt to alter the target URL of a form submission. The form-action directive can restrict where forms are allowed to submit data.
Data Exfiltration
Data exfiltration refers to the unauthorized transfer of data from a website or application. The connect-src directive can restrict which external endpoints JavaScript is allowed to communicate with through Fetch, XMLHttpRequest, WebSocket, or EventSource.
Legacy Plugin Exploits
Although technologies such as Flash and Java Applets are effectively obsolete today, using object-src 'none' remains a good practice because it completely disables embedded plugin-based content.
Don’t Forget the Exceptions
A CSP that is too restrictive can easily break legitimate functionality. For that reason, it is often best to start with the Content-Security-Policy-Report-Only header.
This mode allows you to test your policy without actually blocking scripts or styles. Instead, violations are reported through browser developer tools and can also be sent to reporting endpoints if configured.
Only switch to an enforcing policy after reviewing and addressing all violations related to legitimate functionality.
For example, if you use Sentry for error monitoring or Google Analytics for traffic analysis, make sure the required domains are included in your policy.
Evaluating Your HTTP Security Headers
You can use Mozilla Observatory to evaluate the strength of your site’s security-related HTTP headers and overall configuration.
The Benchmark Comparison section can also be useful for comparing your score against other websites.
You may be surprised by how many well-known sites do not achieve top ratings. In practice, security often requires trade-offs. During one of my evaluations, even Google Search did not receive a particularly high score, which illustrates how complex applications sometimes prioritize functionality and flexibility over strict security policies. Of course, these results may change over time.
Conclusion
Overall, I believe it is worth spending a few hours implementing CSP as an additional layer of defense for your website.
If the site is already in production, introduce rules gradually and use Report-Only mode first to verify that important functionality is not accidentally broken before enforcing the policy.