Skip to Main Content
February 08, 2024

Content Security Policy: Mitigating Web Vulnerabilities by Controlling the Rules of the Game

Written by Drew Kirkpatrick
Application Security Assessment

Defining a Content Security Policy (CSP) for your web application can help harden the application against many common attacks. Mitigating XSS attacks is a significant component of CSP hardening, but CSP can protect against more than XSS attacks. It can set controls to protect against packet sniffing traffic by forcing use of HTTPS and prevent clickjacking attacks by controlling what domains can iframe the application.  

Just to be clear, CSP is a mitigation in most cases. The proper way to address vulnerabilities such as XSS is to fix the actual vulnerability. However, implementing a CSP properly can make exploiting XSS vulnerabilities significantly more difficult for attackers and greatly limit what their payloads can accomplish.

Once an attacker gets their own JavaScript into an application, their malicious code has all the same capabilities as a developer’s own JavaScript within the application. However, restricting what JavaScript can do by using a CSP can let you set the rules of the game, so to speak.

While your application’s JavaScript also must play by these same rules, you can plan for these restrictions and minimize their impact on the development team while greatly limiting attackers.

The best way to set a CSP is in server response headers. It is possible to set a CSP directly in HTML content, but not all features of CSP are available using this approach.

In general, CSPs dictate where specific resource types can be loaded from. CSPs provide not only fine-grained controls, e.g., dictating where fonts can be loaded from, but also ways of applying these restrictions broadly across most resource types.

The default-src directive allows the definition of a default policy, which most specific directives will fall back to. A list of directives and, critically, which browser versions support them can be found on https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP.

Example

Let’s look at a simple CSP and what the impact is for both developers and attackers:

Content-Security-Policy: default-src ‘self’

This simple yet highly restrictive policy tells the browser that all content should come from the application’s own domain, which excludes subdomains.

This will also block inline JavaScript in HTML content. All JavaScript code will have to be loaded from a .js file, and that .js file must be loaded from the application's domain.

This is a huge benefit to security because many XSS vulnerabilities rely on an attacker’s ability to inject malicious JavaScript into the application’s HTML content. By blocking inline JavaScript, the browser honoring the CSP will refuse to execute that malicious code.

What this means for developers is that they’ll need to ensure all JavaScript application logic is hosted inside .js files. While this requires prior planning, this is a perfect example of how using CSP to define the rules that developers and attackers must abide by provides developers with a tremendous advantage over attackers. This is a case of, “This is going to hurt you more than it’s going to hurt me”.

To bypass this restriction, an attacker will need to have their malicious JavaScript in a .js file hosted by the application’s domain (and not a subdomain). If an attacker can upload a .js file with their code, the application allows this filetype to be uploaded, and the file is served with the proper content-type, it might be possible to still get XSS to work. 

The HTML injection will now need to include the hosted malicious JavaScript file instead of the malicious JavaScript directly, e.g., injecting the following HTML after successfully uploading the file:

<script src=”/path/to/file/uploads/trustedSecPayload.js”></script>

This scenario makes exploitation of XSS injection vulnerabilities significantly more difficult.

But wait, there’s more

It gets even worse for the attacker, however.

As an example, let’s say an attacker has overcome these restrictions by finding an appropriate file upload and HTML injection vulnerability and now has their malicious JavaScript code running in the application.

This is a very potent position to be in, as the attacker's JavaScript is running in the context of the application, and the browser trusts it just as much as the application’s own JavaScript. The attacker’s JavaScript can make requests to the application server as the user who is running the malicious JavaScript in their browser.

If the attacker has a highly tailored JavaScript payload that performs a specific action in the application, such as adding a new account the attacker controls, the attack may be successful at this point.

However, many attacks with malicious JavaScript depend on retrieving sensitive data from the application server or client and exfiltrating that data to a server under the attacker’s control. There are many ways this data can be exfiltrated. 

The simplest and cleanest method for the attacker is to run an exfiltration server with an appropriate CORS policy that allows other applications (such as the targeted application) to use JavaScript to communicate with it, which would typically be blocked by same-origin policy.

The malicious JavaScript code could download the sensitive data and simply send it to the exfiltration server using an XMLHttpRequest, Fetch, WebSockets, Beacon, or other JavaScript method of network communication.

This isn’t going to work, however.

CSP has a fantastic directive call, connect-src. It’s one of my personal favorites as it has caused me extreme headaches on engagements. Anything that makes my life difficult as an attacker is a wonderful thing.

This directive controls where HTTP requests created by JavaScript can communicate to, and it uses default-src if it isn’t explicitly defined. This means our example policy only allows communication to the application domain itself and doesn’t even include subdomains.

The attacker’s JavaScript will be blocked from making network connections to its exfiltration domain. DNS requests won’t even be allowed.

There’s another means of exfiltrating data that doesn’t require JavaScript to directly make the network attempts: loading remote resources such as image files.

One may wonder how requesting an image file from a third-party server allows for exfiltration of data. The simplest way is to use the filename itself to contain the data. Really, a successful network connection of any type can serve as a channel for data exfiltration.

In the linked blog post above, data to be exfiltrated is base64 encoded and broken into smaller strings to be used as the “name” of the image file to request from the exfiltration server. These image files are requested, and the server receiving the requests saves the filenames and puts the exfiltrated data back together. Image files do not need to be returned from the server—just getting the request out was enough for the exfiltration to be successful.

This is also not going to work.

CSP has another directive called img-src, and it, too, honors the default-src setting if one isn’t explicitly set. With this directive set to ‘self’, remote images will not be allowed to be loaded, blocking yet another means of data exfiltration.

If the default-src setting for your CSP is less strict, you may want to consider hardening specific directives like connect-src, img-src, font-src, etc., to ensure attackers can’t use these methods to establish network connectivity to third-party domains.

Nonces and Hashes

While the above example demonstrates a very strict policy, there are ways to ease how strict a CSP is, accounting for coding styles and deployment considerations, and still hardening the security posture.

One difficulty in applying a CSP after applications have already been developed is that developers may have already broken the “rules” of a strict CSP.

Inline scripts are a great example. If you need to allow inline scripts, you can still prevent attacker-injected inline scripts by using a nonce. We can modify our CSP to be the following:

Content-Security-Policy: default-src 'self'; script-src 'nonce-ABC123'

This CSP will allow inline scripts if they include the nonce value in the policy, e.g.:

<script nonce=”ABC123”>alert(“Safer inline scripts”);</script>

 Note that the nonce also must be used with JavaScript file includes as well:

<script src=”/libs/jquery.js”
	nonce=”ABC123”
</script>

The downside here is that the nonce must be randomly generated and change every response from the server; otherwise, attackers can predict what the nonce would be. This means a method of automatically changing nonces on the server is necessary.

Developers also cannot cache documents using this method, as they must update every response.

But what if you need caching or your site is static?

Another option is to use hashing instead of a nonce.

If you had the inline script:

<script>alert(“Safer inline scripts”);</script>

 This would be allowed if the hash of this inline script was listed in the CSP. The hash can be calculated:

echo -n 'alert("Safer inline scripts”);' | openssl sha256 | openssl base64

 Providing the output:

U0hBMi0yNTYoc3RkaW4pPSAzZWMyZDJkZWVjYjc2YTIwMWI0YWJmZTgzNjRhOGYw
OTI2MjI1ZGM2MTU5ZjRiZmY1MjI2ZWFkNzg5NThmZmZmCg==

 In the CSP, this script hash could be “allowed” in the CSP, like so:

Content-Security-Policy: default-src 'self'; script-src ‘sha-256-<HASH>’

 or

Content-Security-Policy: default-src 'self'; script-src ‘sha-256- U0hBMi0yNTYoc3RkaW4pPSAzZWMyZDJkZWVjYjc2YTIwMWI0YWJmZTgzNjRhOGYw
OTI2MjI1ZGM2MTU5ZjRiZmY1MjI2ZWFkNzg5NThmZmZmCg==’

Multiple hashes can be included in the CSP.

Hashes can apply to included scripts as well. To do so, the hash will need to be included in the CSP similarly to inline scripts as well as the integrity attribute:

<script src=”/libs/jquery.js”
	integrity=”sha256-<HASH>”
</script>

Beyond XSS

While CSP was primarily created to combat XSS exploitation, it is also helpful for preventing clickjacking attacks and packet sniffing. 

To help ensure that applications are only accessed over secure HTTPS connections, the upgrade-insecure-requests directive forces the browser to automatically redirect any HTTP requests to their HTTPS equivalent. This directive should be used in conjunction with a Strict-Transport-Security header.

Preventing clickjacking is an easy process of not allowing the application to be iframed by another domain:

Content-Security-Policy: default-src 'self'; frame-ancestors ‘self’

This policy states that only the application domain can iframe itself, and no other domain can. This completely protects against clickjacking attacks.

If iframing isn’t needed in the application, a better approach is to set this to none, which can prevent iframe-based XSS persistence and exploitation methods (see here and here):

Content-Security-Policy: default-src 'self'; frame-ancestors ‘none’

Note that frame-ancestors is not one of the directives that will use default-src. You must explicitly set this directive in your CSP, or one will not be applied.

Attackers can sometimes inject new HTML forms into the application that submit the data to an external server under their control. An example would be a fake credit card payment form that overlays the real one on an ecommerce site. This is another example of a directive that does not use the default-src. If you do not set this directive, forms can be submitted anywhere. An example policy would be:

Content-Security-Policy: default-src 'self'; form-action ‘self’

Test before deploying

It is easy to break an application by applying a CSP to it. To help test and debug what a CSP would do to an application at run time, CSP supports a reporting capability. This way you can receive reports of blocking actions that would take place given a policy without blocking the action. 

Defining your policy using this syntax instructs the browser to send a report to the report-to endpoint instead of blocking the action:

Content-Security-Policy-Report-Only: default-src ‘self’; report-to /reports/csp-policy-violations

If this application endpoint is configured to record the JSON data it receives, that JSON will include information on the violation (see here). This way you can iterate on your policy without breaking the application.

In addition to testing your policy using the reporting mechanism, I encourage you to use online CSP policy evaluator tools. You can copy your policy into these tools and receive a report indicating recommended changes. CSP Evaluator is one such tool.

Summary

CSP is a huge topic that expands well beyond the scope of this short blog. There are endless options to tailor precisely what you want to allow and from where. CSP is also an evolving mechanism, with support for directives varying between browsers and versions.

Especially when initially developing a CSP and using the reporting feature, test across browsers that you’re supporting with your application.

And when it comes to XSS, the best defense is not to have the vulnerability in the first place.

If you have any questions or comments, my DMs are always open @hoodoer.

References:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy

https://content-security-policy.com/

https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html

https://csp-evaluator.withgoogle.com