The add-ons team recently completed work to enable Content Security Policy (CSP) on addons.mozilla.org (AMO). This article is intended to cover the basics of implementing CSP, as well as highlighting some of the issues that we ran into implementing CSP on AMO.
What is Content Security Policy?
Content Security Policy (CSP) is a security standard introduced to help prevent cross-site scripting (XSS) and other content injection attacks. It achieves this by restricting the sources of content loaded by the user agent to those only allowed by the site operator.
The policy is implemented via headers that are sent with the server response. From there, it’s up to supporting user agents to take that policy and actively block policy violations as they are detected.
Why is it needed?
CSP is another layer of defense to help protect users from a variety of attack vectors such as XSS and other forms of content injection attacks. While it’s not a silver bullet, it does help make it considerably more difficult for an attacker to inject content and exfiltrate data.
Building websites securely is difficult. Even if you know general web security best practices it’s still incredibly easy to overlook something or unwittingly introduce a security hole in an otherwise secure site.
To implement CSP, you must define lists of allowed origins for the all of the types of resources that your site utilizes. For example, if you have a simple site that needs to load scripts, stylesheets, and images hosted locally, as well as from the jQuery library from their CDN, you could go with:
script-src 'self' https://code.jquery.com;
In the example above, Content-Security-Policy is the HTTP header. You can also specify Content-Security-Policy-Report-Only, which means that the user agent will report errors but not actively block anything. While you’re testing a new policy, this is a useful feature to enable.
It’s very important to always define default-src. Otherwise, the directives will default to allowing all resources. Because we have default-src 'self', this means that images served from the site’s domain will also be allowed.
default-src is a special directive that source directives will fall back to if they aren’t configured. However, the following directives don’t inherit from default-src, so be aware of this and remember that not setting them to anything means they will either be unset or use the browser’s default settings:
Setting 'self' as default-src is generally safe, because you control your own domain. However if you really want to default to locking things down more tightly you could use default-src 'none' and explicitly list all known resource types. Given the example above, this would result in a policy that looks like:
script-src 'self' https://code.jquery.com;
Dealing with inline script
- <script> blocks in the page
- DOM event handlers in HTML e.g: onclick
If you do need to allow it then CSP provides a way to do it safely through the use of the nonce-source or hash-source directives, which allow specific blocks of content to be executed. You can opt out of this protection through the use of ‘unsafe-inline’ in the script-src directive, but this is strongly discouraged as it opens up your site to XSS attacks.
For additional information on nonce-source and hash-source, see CSP for the web we have.
Dealing with eval()
CSP also blocks dynamic script execution such as:
- A string used as the first argument to setTimeout / setInterval
- new Function() constructor
If you need this enabled you can use 'unsafe-eval' but again this is not recommended as it is easy for untrusted code to sneak into eval blocks.
On AMO, we found a lot of library code that used eval and new Function, and this was the part of CSP implementation that took the most time to fix. For example, we had underscore templates that used new Function. Fixing these required us to move to pre-compiled templates.
Dealing with cascading stylesheets (CSS)
CSP defaults to not allowing:
- <style> blocks
- style attributes in HTML
As before, you can use the nonce-source and hash-source directives if you need a controlled approach to allow select pieces of inline CSS.
You’re probably thinking, “This is CSS, what’s the risk?” There are various clever ways that CSS can be used to exfiltrate data from a site. For example, with attribute selectors and background images, you can brute force and exfiltrate attribute sensitive data such as CSRF tokens. For more information on this and other more advanced attack vectors through CSS see XSS (No, the _other ‘S’).
Using 'unsafe-inline' for style-src is not recommended, but it’s a case of balancing the risks against the number of changes that would be necessary to eliminate inline styles.
It’s a good idea to set the report-uri directive and point it somewhere to collect JSON reports of CSP violations. As CSP doesn’t currently coalesce error reports, a single page with multiple errors will result in multiple reports to your reporting endpoint. If you run a site with a large audience, that endpoint can receive a significant amount of traffic.
In addition to reports triggered by actual violations, you’ll also find that many add-ons and browser extensions can cause CSP violations. The net result is a lot of noise: Having something that allows for filtering on the incoming data is highly recommended.
Once you have created your initial policy, the next step is to test it and fix any missing origins. If you run a large site, you may be surprised by the number of sources that you are pulling resources from. Running the site with CSP in report-only mode allows you to catch problems via the console and CSP reports before they actively block things.
Once everyone has confirmed that there’s nothing being blocked erroneously, it’s time to enforce the policy. From there on, it’s just a case of watching out for anything that was missed and keeping the policy up to date with browser support for some of the newer features in CSP.
After you have settled upon a policy that works properly, your next step is to configure your system to deliver the CSP directives. Implementing this varies widely depending on your choice of web server software, but it should generally look like this:
# Enable CSP in Apache Header set Content-Security-Policy "default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com; style-src 'self'"
# Enable CSP in nginx add_header Content-Security-Policy "default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com; style-src 'self'";
If your service provider doesn’t offer control over your web server’s configuration, don’t panic! You can still enable CSP through the use of meta tags. Simply have your meta tag be the first tag inside <head>:
<!-- Enable CSP inside the page's HTML --> <head> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self'; script-src 'self' https://code.jquery.com; style-src 'self'"> </head>
Our final implementation
Given that AMO is an older and extremely complex site, you’re probably curious as to what our final policy ended up looking like:
connect-src 'self' https://sentry.prod.mozaws.net;
font-src 'self' https://addons.cdn.mozilla.net;
frame-src 'self' https://ic.paypal.com https://paypal.com
img-src 'self' data: blob: https://www.paypal.com https://ssl.google-analytics.com
script-src 'self' https://addons.mozilla.org
style-src 'self' 'unsafe-inline' https://addons.cdn.mozilla.net;
Wow! As you can imagine, quite a lot of testing went into discovering the myriad resources that AMO utilizes.
The older your site is, the more work it will take to set and adhere to a reasonable Content Security Policy. However, the time is worth spending as it’s an additional layer of security that supports the idea of defense in depth.
- An Introduction to Content Security Policy, by Mike West
- Browser Support for CSP
- Content Security Policy Level 2 Specification
- MDN docs on CSP
View full post on Mozilla Hacks – the Web developer blog