Content Security Policy

Introduction

Content Security Policy (CSP) is a security standard introduced to prevent cross-site scripting (XSS), clickjacking and other code injection attacks resulting of malicious content being executed in the trusted web page context.

Think of CSP in terms of an "allow/deny" list for remote contents.

CSP rules are used to describe, which external assets or functionality are allowed for certain HTML tags (like <script>, <img>, <iframe>). This allows to restrict external resources or JavaScript execution with security in mind. When accessing a page, these rules are sent as part of the HTTP request from the server to the browser, and the browser will enforce these rules (and reject non-allowed content). These rejection are always logged in the browser console. Additionally, external tools can be configured to receive and track violations of the policy.

Content Security Policy declarations can be applied to a TYPO3 website in frontend and backend scope with a dedicated API. This API allows for site-specific or extension-specific configuration instead of manually setting the CSP rules with server-side configuration through httpd.conf/nginx.conf or .htaccess files.

To delegate Content Security Policy handling to the TYPO3 frontend, at least one of the feature flags:

needs to be enabled, or the site-specific csp.yaml configuration file needs to set the enforce or reporting disposition like this:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  # site-specific mutations could also be listed, like this:
  # mutations:
  #  - mode: "append"
  #    directive: "img-src"
  #    sources:
  #      - "cdn.example.com"
  #      - "assets.example.com"

# alternatively (or additionally!), reporting can be set too,
# for example when testing stricter rules than above
# (note the missing 'assets.example.com')
# reporting:
#   inheritDefault: true
#   mutations:
#    - mode: "append"
#      directive: "img-src"
#      sources:
#        - "cdn.example.com"
#
Copied!

Changed in version 13.0

In the TYPO3 backend the Content Security Policy is always enforced.

Within the TYPO3 backend, a specific backend module is available to inspect policy violations / reports, and there is also a list to see all configured CSP rules, see section Active content security policy rules.

Terminology

This document will use very specific wording that is part of the CSP W3C specification, these terms are not "invented" by TYPO3. Since reading the W3C RFC can be very intimidating, here are a few key concepts.

Directives

  • CSP consists of multiple rules (or "directives"), that are part of a "policy". This policy says, what functionality the site's output is allowed to use.
  • With that, several HTML tags can be controlled, like from which URLs images can be requested from, if and from where iframes are allowed, if and from where JavaScripts are allowed and so on. There is a long list of applicable directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives with specific identifiers like default-src, img-src and so on.
  • Each directive may have several "attributes" (like the allowed URLs).
  • Directives may build upon each other, a bit like CSS definitions (Cascading Style Sheet) do. However, these are more meant to modify a basic rule on an earlier level, and called "mutations". The relation of a "child rule" to its "parent" is also called "ancestor chain". An example:

    If frame-src is not defined, it falls back to child-src, and finally falls back to default-src. But if frame-src is defined, it is used, and the sources from default-src are not used. In such a case, default-src listed sources have to be repeated (when wanted) explicitly in frame-src.

Applying the policy

  • A final policy is compiled of all these directives, and then sent as a HTTP response header Content-Security-Policy: ... (respectively Content-Security-Policy-Reporty-Only).
  • In TYPO3, directives can be specified via PHP syntax (within Extensions) and YAML syntax (within site configuration). Additionally, rules can be set via the PSR-14 event PolicyMutatedEvent.

Mutations

  • These rules can influence each other, this is where the concept of "mutations" come in. The "policy builder" of TYPO3 applies each configured mutation, no matter where it was defined.
  • Because of this, each mutation (directive definition) needs a specific "mode" that can instruct, how this mutation is applied: Should an existing directive be set, inherited, appended, remove or extended to the final policy (see Content Security Police modes).
  • Each directive is then applied in regard to its defined mode and can list one or more "sources" with the values of additional parameters of a directive. Sources are web site addresses / URLs (or just protocols), and also include some special keywords like self/none/data:.

Nonces

  • There are possible exemptions to directives for specific content created on specific pages created by TYPO3 in your frontend (or backend modules). To verify, that these exemptions are valid in a policy, a so-called "Nonce" (a unique "number used once") is created (details on https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce).

    • TYPO3 can manage these Nonces and apply them were configured.
    • Nonces are retrieved from \TYPO3\CMS\Core\Security\ContentSecurityPolicy\ConsumableNonce and will be used for any directive within the scope of a single HTTP request.
    • More details are covered in Nonce.

Policy violations and reporting

  • When a webpage with activated policies is shown in a client's browser, each HTML tag violating the policy will not be interpreted by the browser.
  • Depending on a configuration of a possible "Report", such violations can be submitted back to a server and be evaluated there. TYPO3 provides such an endpoint to receive and display reports in a backend module, but also third-party servers are usable.
  • Policies can be declared with "dispositions", to indicate how they are handled. "Enforce" means that a policy is in effect, and "Report" allows to only pretend a policy is in effect, to gather knowledge about possible improvements of a webpage's output. Both dispositions can be set independently in TYPO3.
  • All active rules can be seen in the backend configuration section, see Active content security policy rules.

Example scenario

Let's define a small real-world scenario:

  • You have one TYPO3 installation with two sites (frontend), example.com and example.org.
  • You have created custom backend modules for some distinct functionality.
  • example.com is a site where your editors fully control all frontend content, and they want to have full flexibility of what and how to embed. You use a CDN network to deliver your own large assets, like videos.
  • example.org is a community-driven site, where users can manage profiles and post chats, and where you want to prevent exploits on your site. Some embedding to a set of allowed web services (YouTube, Google Analytics) must be possible.

So you need to take care of security measures, and find a pragmatic way how to allow foreign content (like YouTube, widgets, tracking codes, assets)

Specifically you want to to set these following rules, as an example.

Rules for example.com (editorial)

  • <iframe> to many services should be allowed
  • <img> sources to anywhere should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for example.org (community)

  • <iframe> to cdn.example.com, *.youtube.com should be allowed
  • <img> sources to cdn.example.com and *.instagram.com should be allowed
  • <script> sources to cdn.example.com and *.youtube.com and *.google.com should be allowed

Rules for the TYPO3 backend

Normal TYPO3 backend rules need to be applied, so we only want to add some rules for custom backend modules:

  • <iframe> to cdn.example.com should be allowed
  • <img> sources to cdn.example.com should be allowed
  • <script> sources to cdn.example.com should be allowed

Resulting configuration example:

And this is how you would do that with a CSP YAML configuration file, one per site:

config/sites/example-com/csp.yaml | typo3conf/sites/example-com/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frames/iframes to TRUSTED specific locations
  # Avoid "protocol only" white-list like "https:" here,
  # because it could inject javascript easily, the most important reason
  # why CSP was invented was to block security issues like this.
  # (Note: it's "frame-src" not "iframe-src")
  - mode: "extend"
    directive: "frame-src"
    sources:
      - "https://*.example.org"
      - "https://*.example.com"
      - "https://*.instagram.com"
      - "https://*.vimeo.com"
      - "https://*.youtube.com"

  # Allow img src to anyhwere (HTTPS only, not HTTP)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https:"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
config/sites/example-org/csp.yaml | typo3conf/sites/example-org/csp.yaml
# Inherits default frontend policy mutations provided by Core and 3rd-party extensions (enabled per default)
inheritDefault: true
mutations:
  # Allow frame/iframe src to the specified domains (HTTPS only)
  - mode: "extend"
    # (Note: it's "frame-src" not "iframe-src")
    directive: "frame-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"

  # Allow img src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "img-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.instagram.com"

  # Allow script src to the specified domains (HTTPS only)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "https://cdn.example.com"
      - "https://*.youtube.com"
      - "https://*.google.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    // Provide declarations for the backend only
    Scope::backend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Extend,
            // Note: it's "FrameSrc" not "IFrameSrc"
            Directive::FrameSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('https://cdn.example.com'),
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ScriptSrc,
            new UriValue('https://cdn.example.com'),
        ),
    ),
]);
Copied!

This is really just a simple demo, that has room for improvements. For example, the allowed list of *-src values to any directive could actually be set through their common parent, the default-src attribute. There is a very deep and nested possibility to address the attributes of many HTML5 tags, which is covered in depth on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives.

You can take a look into the PHP enum \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive , which gives you an overview of all supported directives.

Read on to understand more of the underlying API builder concepts below.

Configuration

Policy builder approach

The following approach illustrates how a policy is build:

<?php

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Security\Nonce;

$nonce = Nonce::create();
$policy = (new Policy())
    // Results in `default-src 'self'`
    ->default(SourceKeyword::self)

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `img-src 'self' data: https://*.typo3.org`
    ->extend(Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org'))

    // Extends the ancestor directive ('default-src'),
    // thus reuses 'self' and adds additional sources
    // Results in `script-src 'self' 'nonce-[random]'`
    // ('nonce-proxy' is substituted when compiling the policy)
    ->extend(Directive::ScriptSrc, SourceKeyword::nonceProxy)

    // Sets (overrides) the directive,
    // thus ignores 'self' of the 'default-src' directive
    // Results in `worker-src blob:`
    ->set(Directive::WorkerSrc, SourceScheme::blob);

header('Content-Security-Policy: ' . $policy->compile($nonce));
Copied!

The result of the compiled and serialized result as HTTP header would look similar to this (the following sections are using the same example, but utilize different techniques for the declarations):

Content-Security-Policy: default-src 'self';
    img-src 'self' data: https://*.typo3.org; script-src 'self' 'nonce-[random]';
    worker-src blob:
Copied!

Extension-specific

Policies for frontend and backend can be applied automatically by providing a Configuration/ContentSecurityPolicies.php file in an extension, for example:

EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries(
    [
        // Provide declarations for the backend
        Scope::backend(),
        // NOTICE: When using `MutationMode::Set` existing declarations will be overridden

        new MutationCollection(
            // Results in `default-src 'self'`
            new Mutation(
                MutationMode::Set,
                Directive::DefaultSrc,
                SourceKeyword::self,
            ),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `img-src 'self' data: https://*.typo3.org`
            new Mutation(
                MutationMode::Extend,
                Directive::ImgSrc,
                SourceScheme::data,
                new UriValue('https://*.typo3.org'),
            ),
            // NOTICE: the following two instructions for `Directive::ImgSrc` are identical to the previous instruction,
            // `MutationMode::Extend` is a shortcut for `MutationMode::InheritOnce` and `MutationMode::Append`
            // new Mutation(MutationMode::InheritOnce, Directive::ImgSrc, SourceScheme::data),
            // new Mutation(MutationMode::Append, Directive::ImgSrc, SourceScheme::data, new UriValue('https://*.typo3.org')),

            // Extends the ancestor directive ('default-src'),
            // thus reuses 'self' and adds additional sources
            // Results in `script-src 'self' 'nonce-[random]'`
            // ('nonce-proxy' is substituted when compiling the policy)
            new Mutation(
                MutationMode::Extend,
                Directive::ScriptSrc,
                SourceKeyword::nonceProxy,
            ),

            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src blob:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                SourceScheme::blob,
            ),
        ),
    ],
    [
        // You can also additionally provide frontend declarations
        Scope::frontend(),
        new MutationCollection(
            // Sets (overrides) the directive,
            // thus ignores 'self' of the 'default-src' directive
            // Results in `worker-src https://*.workers.example.com:`
            new Mutation(
                MutationMode::Set,
                Directive::WorkerSrc,
                new UriValue('https://*.workers.example.com'),
            ),
        ),
    ],
);
Copied!

The API here is much like the YAML syntax. The PHP code needs to return a mapped array of an MutationCollection instance with all rules put into a sub-array, containing instances of a single Mutation.

Each Mutation instance is like a Data Object (DO) where its constructor allows you to specifiy a mode (type MutationMode), a directive (type Directive) and one ore more actual values ("sources", type UriValue or SourceKeyword).

Additionally, a Scope instance object is included, which can either be Scope::backend() or Scope::frontend().

A good PHP IDE will allow for good autocompletion and hinting, and using a boilerplate configuration like the example above helps you to get started.

Backend-specific

The YAML configuration only applies to the frontend part of TYPO3. Backend policies need to be set using the PHP API, within an extension as described in the section above.

You need to ensure that Scope::backend() is set in the mapped return array for the rules you want to setup.

Site-specific (frontend)

In frontend, a dedicated sites/<my_site>/csp.yaml can be used to declare policies for a specific site, for example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# Inherits default site-unspecific frontend policy mutations (enabled per default)
inheritDefault: true
mutations:
  # Results in `default-src 'self'`
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `img-src 'self' data: https://*.typo3.org`
  - mode: "extend"
    directive: "img-src"
    sources:
      - "data:"
      - "https://*.typo3.org"

  # Extends the ancestor directive ('default-src'),
  # thus reuses 'self' and adds additional sources
  # Results in `script-src 'self' 'nonce-[random]'`
  # ('nonce-proxy' is substituted when compiling the policy)
  - mode: "extend"
    directive: "script-src"
    sources:
      - "'nonce-proxy'"

  # Results in `worker-src blob:`
  - mode: "set"
    directive: "worker-src"
    sources:
      - "blob:"
Copied!

Disable CSP for a site

The Content Security Policy for a particular site can be disabled with the active key set to false:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
# "active" is enabled by default if omitted
active: false
Copied!

Site-specific Content-Security-Policy endpoints

The reporting endpoint is used to receive browser reports about violations to the security policy, for example if a YouTube URL was requested, but could not be displayed in an iframe due to a directive not allowing this.

Reports like this can help to gain insight, what URLs are used by editors and might need inclusion into the policy.

Since reports can be sent by any browser, they can possibly easily flood a site with requests and take up storage space. Reports are stored in the sys_http_report database table when using the endpoint provided by TYPO3.

To influence whether this endpoint accepts reports, the disposition-specific property reportingUrl can be configured and set to either:

true
to enable the reporting endpoint
false
to disable the reporting endpoint
(string)
to use the given value as external reporting endpoint

If defined, the site-specific configuration takes precedence over the global configuration contentSecurityPolicyReportingUrl.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

Changed in version 12.4.27 / 13.4.5

Example: Disabling the reporting endpoint
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: false
Copied!
Example: Using custom external reporting endpoint
config/sites/<my-site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
enforce:
  inheritDefault: true
  mutations: {}
  reportingUrl: https://example.org/csp-report
Copied!

Content Security Police modes

Adjusting specific directives / mutations for a policy can be performed via the following modes:

append

append
YAML
append
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Append

Appends to a given directive.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "example.org"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            new UriValue('example.org'),
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src example.org example.com
Copied!

extend

extend
YAML
extend
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Extend

Extends the given directive. It is a shortcut for inherit-once and append.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "extend"
    directive: "img-src"
    sources:
      - "example.com"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Extend,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self'; img-src 'self' example.com
Copied!

inherit-again

inherit-again
YAML
inherit-again
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritAgain

Inherits again from the corresponding ancestor chain and merges existing sources.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-again"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-again"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritAgain,
            Directive::ScriptSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src data: 'self' example.com
Copied!

Note that data: is inherited to img-src (in opposite to inherit-once).

inherit-once

inherit-once
YAML
inherit-once
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::InheritOnce

Inherits once from the corresponding ancestor chain. When inherit-once is called multiple times on the same directive, only the first time is applied.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
inheritDefault: false
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "inherit-once"
    directive: "img-src"

  - mode: "append"
    directive: "img-src"
    sources:
      - "example.com"

  - mode: "set"
    directive: "default-src"
    sources:
      - "data:"

  - mode: "inherit-once"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
        new Mutation(
            MutationMode::Append,
            Directive::ImgSrc,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::InheritOnce,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src data:; img-src 'self' example.com
Copied!

Note that data: is not inherited to img-src. If you want to inherit also data: to img-src use inherit-again.

reduce

reduce
YAML
reduce
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Reduce

Reduces a directive by a given aspect.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
      - "data:"
      - "example.com"

  - mode: "reduce"
    directive: "img-src"
    sources:
      - "data:"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\UriValue;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
            SourceScheme::data,
            new UriValue('example.com'),
        ),
        new Mutation(
            MutationMode::Reduce,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: default-src 'self' example.com
Copied!

remove

remove
YAML
remove
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Remove

Removes a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "default-src"
    sources:
      - "'self'"

  - mode: "set"
    directive: "img-src"
    sources:
      - "data:"

  - mode: "remove"
    directive: "img-src"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceScheme;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::DefaultSrc,
            SourceKeyword::self,
        ),
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceScheme::data,
        ),
        new Mutation(
            MutationMode::Remove,
            Directive::ImgSrc,
        ),
    ),
]);
Copied!

Results in:

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

set

set
YAML
set
PHP
\TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode::Set

Sets (overrides) a directive completely.

Example:

config/sites/<my_site>/csp.yaml | typo3conf/sites/<my_site>/csp.yaml
mutations:
  - mode: "set"
    directive: "img-src"
    sources:
      - "'self'"
Copied!
EXT:my_extension/Configuration/ContentSecurityPolicies.php
<?php

declare(strict_types=1);

use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Mutation;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationCollection;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationMode;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\SourceKeyword;
use TYPO3\CMS\Core\Type\Map;

return Map::fromEntries([
    Scope::frontend(),
    new MutationCollection(
        new Mutation(
            MutationMode::Set,
            Directive::ImgSrc,
            SourceKeyword::self,
        ),
    ),
]);
Copied!

Results in:

Content-Security-Policy: img-src 'self'
Copied!

Nonce

The nonce attribute is useful to allowlist specific elements, such as a particular inline script or style elements. It can help you to avoid using the CSP unsafe-inline directive, which would allowlist all inline scripts or styles.

-- MDN Web Docs, https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce

It may look like this in your HTML code:

<link
    rel="stylesheet"
    href="/_assets/af46f1853e4e259cbb8ebcb816eb0403/Css/styles.css?1687696548"
    media="all"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
>

<style nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline styles */
</style>

<script
    src="/_assets/27334a649e36d0032b969fa8830590c2/JavaScript/scripts.js?1684880443"
    nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw"
></script>

<script nonce="sqK8LkqFp-aWHc7jkHQ4aT-RlUp5cde9ZW0F0-BlrQbExX-PRMoTkw">
    /* some inline JavaScript */
</script>
Copied!

The nonce changes with each request so that (possibly malicious) inline scripts or styles are blocked by the browser.

The nonce is applied automatically, when scripts or styles are defined with the TYPO3 API, like TypoScript ( page.includeJS, etc.) or the asset collector. This only refers to referenced files (via src and href attributes) and not inline scripts or inline styles. For those, you should either use the PHP/Fluid approach as listed below, or use TypoScript only for passing DOM attributes and using external scripts to actually evaluate these attributes to control functionality.

TYPO3 provides APIs to get the nonce for the current request:

Retrieve with PHP

The nonce can be retrieved via the nonce request attribute:

// use TYPO3\CMS\Core\Domain\ConsumableString

/** @var ConsumableString|null $nonce */
$nonceAttribute = $this->request->getAttribute('nonce');
if ($nonceAttribute instanceof ConsumableString) {
    $nonce = $nonceAttribute->consume();
}
Copied!

In a Fluid template

The f:security.nonce ViewHelper is available, which provides the nonce in a Fluid template, for example:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<script nonce="{f:security.nonce()}">
    const inline = 'script';
</script>

<style nonce="{f:security.nonce()}">
    .some-style { color: red; }
</style>
Copied!

You can also use the f:asset.script or f:asset.css ViewHelpers with the useNonce attribute:

EXT:my_extension/Resources/Private/Templates/SomeTemplate.html
<f:asset.script identifier="my-inline-script" useNonce="1">
    const inline = 'script';
</f:asset.script>

<f:asset.css identifier="my-inline-style" useNonce="1">
    .some-style { color: red; }
</f:asset.css>
Copied!

Notes about nonces and caching

Nonces are implemented via a PSR middleware and thus applied dynamically. This also means, they are somewhat "bad" for caching (especially for reverse proxies), since they create unique output for a specific visitor.

Since the goal of nonces are to allow "exemptions" for otherwise forbidden content, this closely relates to validity or integrity of this forbidden content. Instead of emitting unique nonces, another possibility is to utilize hashing functionality to content regarded as "safe".

This can be done with sha256/sha384/sha512 hashing of referenced script, and including them as a valid directive, like this:

The "sha256-..." block would be the SHA256 hash created from a file like 'script.js'.

For example, a file like this:

script.js (some javascript file that is included in your website)
console.log('Hello.');
Copied!

would correspond to a SHA256 hash of 6c7d3c1bf856597a2c8ae2ca7498cb4454a32286670b20cf36202fa578b491a9.

The browser would evaluate a reference JavaScript file and calculate it's SHA256 hash and compare it to the list of allowed hashes.

The downside of this is: Everytime an embedded file changes (like via build processes), the CSP SHA hash would need to be adopted. This could be automated by a PHP definition of CSP rules and hashing files automatically, which would be a performance-intense process and call for its own caching.

There is no automatism for this kind of hashing in TYPO3 (yet, see https://forge.typo3.org/issues/100887), so it has to be done manually as outlined above.

Reporting of violations, CSP Backend module

Potential CSP violations are reported back to the TYPO3 system and persisted internally in the database table sys_http_report. A corresponding Admin Tools > Content Security Policy backend module supports users to keep track of recent violations and - if applicable - to select potential resolutions (stored in the database table sys_csp_resolution) which extends the Content Security Policy for the given scope during runtime:

Backend module "Content Security Policy" which displays the violations

Clicking on a row displays the details of this violation on the right side including suggestions on how to resolve this violation. You have the choice to apply this suggestion, or to mute or delete the specific violation.

Using a third-party service

As an alternative to the built-in reporting module, an external reporting URL can be configured to use a third-party service as well:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl']
    = 'https://csp-violation.example.org/';
Copied!

Violations are then sent to the third-party service instead of the TYPO3 endpoint. Resolutions would then not be applied dynamically.

Disabling content security policy reporting globally

Administrators can disable the reporting endpoint globally or configure it per site as needed. (See Example: Disabling the reporting endpoint).

If defined, the site-specific configuration takes precedence over the global configuration.

In case the explicitly disabled endpoint still would be called, the server-side process responds with a 403 HTTP error message.

The global scope-specific setting contentSecurityPolicyReportingUrl can be set to zero ('0') to disable the CSP reporting endpoint:

config/system/additional.php
// For backend
$GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] = '0';

// For frontend
$GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] = '0';
Copied!

Active content security policy rules

The backend module System > Configuration > Content Security Policy Mutations uses a simple tree display of all configured directives, grouped by frontend or backend. Each rule shows where it is defined, and what its final policy is set to:

Backend module "Configuration > Content Security Policy Mutations" which displays a tree of all policy directives.

PSR-14 events

The following PSR-14 events are available: