Why should you care about Content Security Policy?

Content security policy (CSP) is a powerful tool which can effectively stop Cross-Site Scripting attacks. Since the implementation is not the easiest task, check our approach on how to deal with it.

Michał Ogorzałek 2021.03.22   –   14 MIN read

Content Security Policy (CSP) is like a bouncer in a club. It legitimizes every resource and lets in only the ones who have a valid ticket or meet the requirements to enter – trusted and without any malicious intentions. Officially, CSP is a security standard which helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS). 

However, that’s where the easy part ends and the questions begin. CSP is a complex topic and there are many misconceptions about how it should look like. Should it just be a whitelist of allowed domains? What is going on with these nonces and hashes? One can be easily discouraged after seeing all discrepancies, different approaches, and possible problems.

Also, CSP does not mean that you have to get rid of all external resources or inline scripts. Rather, you have to identify every script and resource in order to tag it appropriately.

We prepared this guide to help you deeply understand CSP and choose your approach wisely.

This article discusses problems related to implementation of CSP. To fully understand various directives and sources, a new policy is created from scratch in the Getting started with CSP section. If you are interested only in the ready to go solution, scroll to the Our final policy section.

CSP in real life

Most often CSP is defined by the header added to HTTP responses. The header creates security rules which instruct browsers what resources (scripts, images, styles…) can be loaded and helps to stop some attacks.

If the policy is already implemented, it is often used in a monitoring mode, or it is so wide that a Cross-Site Scripting attack can still be carried out without any additional effort. On the other hand, sometimes we are not able to exploit some vulnerabilities since the policy is so strict and well implemented.

Our experience shows that the mechanism is both useful and problematic in implementation and thus it is worth spending some time on it. 

Why is implementing CSP so hard? 

1. It takes a lot of time

Implementing Content Security Policy is not an easy task. Adding the policy header is not enough to implement the mechanism properly – usually reorganization and refactoring of the application code is required. Inline scripts must be moved to external files or a nonce attribute must be added to <script> elements. Then inline event handlers and javascript URIs must be rewritten. Therefore the implementation cannot be treated as a one-time job, but more as a process. Developers need to be aware of the mechanism and develop applications according to specific rules. If only we had CSP in mind at the beginning of the development process… 

2. The application doesn’t work anyway

When there are a lot of things to remember and major changes are implemented, it is easy to make mistakes. If somebody forgets to include a required domain in CSP’s whitelist, the needed resources will not be loaded by browser and end users will not be able to use the application. Similar problems can occur if backward compatibility will not be taken into consideration, since support for Content Security Policy differs in various browsers. 

3. The application is still vulnerable! 

CSP does not eliminate XSS-type vulnerabilities, but blocks execution of scripts that do not follow their rules. An attacker trying to exploit the vulnerability will try to bypass the security mechanism and find a way to load malicious script that works with the CSP. 

The Content Security Policy on the screenshot from CSP Evaluator tool above contains the most common gaps. 

  • The unsafe-inline source allows inline script tags and execution of JavaScript event handlers. As a result, CSP does not block classical XSS-es like <script>alert(‘xss’);<script>.
  • It is also good to get rid of unsafe-eval source as it allows the use of eval() and some other functions, which can also be used during the XSS attack. However, disabling eval can be hard, as it is necessary for some third-party javascript libraries like jQuery. 
  • Also, there are some URL addresses defined as trusted script sources (*.securing.pl, *.googleadservices.com, gstatic.com). Even if the hosts are trusted, they still can be used to perform the attack, if they, for example, host a JSONP endpoint or Angular.
  • Script-src is not the only directive which is essential in context of blocking of XSS attacks. A malicious code can be executed also through HTML elements controlled by object-src directive.

4. Our CSP can be bypassed anyway

Even if we do our best and implement Content Security Policy properly, IT security researchers manage to bypass the restrictions using so-called script-gadgets and show us that nothing really works. So the question is, what to do in this madness? – We have the optimal solution, but let’s start from the beginning. 

Getting started with CSP

Let’s start with the following policy:

Content-Security-Policy:
default-src 'none';
script-src 'self';
connect-src 'self';
img-src 'self';
style-src 'self';
base-uri 'self';
form-action 'self'

It allows images, scripts, AJAX etc. only from the same origin, so it will probably break your application – that’s why we have to customize it first.

Each of the -src directives defines possible sources for objects of a specific type.

We will begin with describing the sources of the scripts.

Setting up script-src directive:

script-src * 'unsafe-inline' 'unsafe-eval'

What does it do: Allows loading scripts from every domain (*), executing inline scripts such as <script> tags, javascript: URIs, event handlers etc. (unsafe-inline) and dynamic code evaluation (unsafe-eval).

Pros: Works for almost all applications.
Cons: Does not protect against anything.

script-src 'self' alice.example.com bob.example.com 'unsafe-inline' 'unsafe-eval'

What does it do: Allows loading scripts from the same origin (self) and specified domains (alice.example.com, bob.example.com), executing inline scripts (unsafe-inline) and dynamic code evaluation (unsafe-eval).

You can also specify the protocol to be HTTPS (e.g. https://alice.example.com).

Pros: Still pretty easy and works for almost all applications. Prevents the attacker from loading scripts from the attacker’s domain.
Cons: Does not protect against inline XSS. 

script-src 'self' 'nonce-c2VjdXJpbmc=' alice.example.com bob.example.com 'unsafe-inline' 'unsafe-eval'

In this case, a nonce is introduced, which is a random value generated by the server. Every HTTP response requires a new nonce. All scripts in a response must have the same nonce to be loaded. 

What does it do: Allows loading scripts from the same origin (self) and specified domains (alice.example.com, bob.example.com). It allows dynamic code evaluation (unsafe-eval). Inline <script> tags will only be executed, if they have proper nonce value (as specified in the CSP header) e.g. <script nonce=nonce-c2VjdXJpbmc=>…</script>. Other inline scripts (javascript: URIs, event handlers) will not work. If the nonce is specified, modern browsers will ignore unsafe-inline – however, it can still be set to maintain compatibility with older browsers. 

Pros: Protects against inline XSS, as long as it is not inside <script> tags. Works in all browsers (but only the modern browsers are protected with the nonce mechanism). 
Cons: The CSP can sometimes be bypassed, if a whitelisted domain serves JSONP replies or Angular libraries (e.g. if your whitelist contains ajax.googleapis.com).

script-src 'self' 'nonce-c2VjdXJpbmc=' 'strict-dynamic' alice.example.com bob.example.com 'unsafe-inline' 'unsafe-eval'

What does it do: A new level of protection is applied – strict-dynamic means that it is only possible to include a script from an external domain, if it is imported in a <script> tag with a nonce. The trust is propagated to all the scripts loaded by the trusted script. The domain whitelist and unsafe-inline are only included for compatibility reasons. Best we can do is to use a whitelist of allowed domains as well as nonces, hashes and strict-dynamic – Firefox and Chrome will ignore the whitelist, while Safari or Internet Explorer will ignore strict-dynamic and use the whitelist.

Pros: Protects against inline XSS, as long as it is not inside <script> tags. Protects against CSP bypasses (but only in modern browsers).
Cons: CSP can still sometimes be bypassed, if the application is run in a browser that does not support strict-dynamic and the domain whitelist e.g. contains JSONP endpoints.

If we don’t care for compatibility, we can as well shorten this policy and include only the following:

script-src 'nonce-c2VjdXJpbmc=' strict-dynamic 'unsafe-eval'

Pros: Protects against inline XSS, as long as it is not inside <script> tags. Protects against CSP bypasses (but only in modern browsers).
Cons: Does not work in browsers that don’t support strict-dynamic (e.g. Safari).

After successfully implementing the above configuration, there are still places to go – e.g. we can try to eliminate unsafe-eval (it is used in many analytics scripts, so it might not be so easy) or implement Subresource Integrity (it protects the application against a scenario in which the attacker takes control over an external CDN and modifies the scripts). We should also keep in mind that the state of Content Security Policy is dynamically changing, as more and more browsers are implementing it.

Other directives:

As we defined the sources of the scripts in the application, the rest is much easier. The following directives are left from our initial configuration:

  • connect-src – applies to AJAX requests, WebSockets, etc.
  • img-src – sources of images.
  • style-src – sources of stylesheets.
  • base-uri – defines possible values for HTML base tag (default target for all relative URLs in a document).
  • form-action – defines URLs that can be used as targets for HTML forms.

The sources can be defined in the same way as script sources – we can list allowed domains, restrict them to the same origin, etc. However, as the list of possible directives is much longer, the undefined ones will be fully disabled by default (default-src: ‘none’) according to the default deny principle.

Why should I use an allowlist in case of CSP?

During our research about CSP we noticed that some recommended policies do not include the default-src directive, or use default-src ‘self’ and block e.g. only object-src. However, one of the most important of the security best practices is the default deny principle, which means that we should never use blocklists as a security measure. We think that in case of CSP it’s better to use an allowlist approach, especially considering the fact that the CSP standard is still evolving and changing dynamically, so there’s a possibility that new directives can be added in the future, making us exposed to new vulnerabilities due to the lack of restrictive default-src.

Let’s get some feedback – CSP reporting

Another important feature of CSP is reporting. 

report-uri https://csp.example.com

By adding the report-uri directive, all the policy violations will be reported to the provided URL (in this case: https://csp.example.com).

It is also possible to implement CSP in a report-only mode, which allows for testing the new policy while not blocking the incompatible resources. In order to do so, the Content-Security-Policy-Report-Only header can be used.

The Ultimate Content Security Policy

Using the above guide, the following policy was created:

Content-Security-Policy:
default-src 'none';
script-src 'nonce-c2VjdXJpbmc=' 'strict-dynamic' 'unsafe-eval';
connect-src 'self';
img-src 'self' data:;
style-src 'self' 'unsafe-inline';
base-uri 'self';
form-action 'self';
report-uri https://csp.example.com;

To summarize:

  • It offers protection against many XSS attacks (attacker-defined <script> tags, inline JavaScript). It allows including a script from an external domain, if it is imported in a <script> tag with a valid nonce. However, XSS can happen inside a valid <script> tag, if it uses user-supplied content.
  • The unsafe-eval tag allows the application to use most of the analytics scripts and popular third-party libraries without any modifications.
  • The following are restricted to the application URL: 
    • The sources for stylesheets (style-src) and images (img-src),
    • The targets for AJAX requests, WebSockets connections, etc. (connect-src),
    • All forms (form-action) and relative links (base-uri) – since these directives do not fall back to default-src.
  • The CSP also allows for inline CSS and images embedded as data:.
  • All other directives are set by default to none.
  • All violations of the CSP are reported to https://csp.example.com.
  • The above policy will not work in some browsers which don’t support strict-dynamic expression. For backward compatibility, the following sources can be added to the script-src directive:
'unsafe-inline' https: http:

Let’s take a look at the evaluation of our policy using CSP Evaluator

Reasonable approach to implementing CSP

Let’s get this straight – implementing CSP can be really time consuming. Afterall, it is a process, not a one-time job. Furthermore, sometimes the security requirements for the application might define the need of CSP only in specific places in the application. It is particularly useful to restrict script sources when we allow the user to create arbitrary HTML content (e.g. using WYSIWYG editors) or for critical functionalities (e.g. log viewing panel in the administrator interface). Here you can find more information about the process of adopting CSP. 

What about compatibility? 

Keep in mind that browsers differ in supporting the CSP standard. Currently, CSP level 2, which adds hashes and nonces, is supported in almost all browsers. According to Can I use, almost 94% of users use browsers that support this version of the standard. 

The strict-dynamic expression, which is part of published in 2018 CSP level 3 and is used in the recommended policy, is supported by browsers used by 74% of users. If the browser does not support a particular expression, it is simply ignored, which results in lack of protection or inability to use the application, so consider backward compatibility.  

In the process of CSP implementation, the following tools can be useful:

Another case of CSP – let’s stop the tracking

Just to provide some food for thought, we wanted to share with you our own CSP story. As we don’t like being tracked, we recently implemented CSP to stop one of our internal applications from sending any analytics to the outside world. In this case, we didn’t care about XSS-es (and also didn’t want the application to stop working), so we simply used an allowlist of domains with unsafe-inline and unsafe-eval. No more Big Brother for us! 

What to do next?

Content security policy (CSP) can be both a blessing and a curse. There are many factors which need to be taken into consideration during the implementation process of CSP, the process that is not simple and often requires code changes. However, since well implemented policy can effectively block Cross-Site Scripting (XSS) attacks and increase the security of the application, we think that it is worth the effort. 

CSP is a powerful tool, but remember, you shouldn’t relax the policies of encoding and validating data. XSS vulnerabilities must be solved at the source – CSP just offers additional protection if any mistake is made.  

Remember that the mechanism shows its greatest power when it is developed in projects from the very beginning, so do not hesitate to pitch it during your next meeting.

If you have more questions or would like to discuss CSP implementation in your case, feel free to use our contact form.

Special thanks to the co-author of the article:
Natalia Trojanowska

Michał Ogorzałek
Michał Ogorzałek IT Security Consultant