Content Security Policy

New in version 12.3

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.

Content Security Policy declarations can be applied to a TYPO3 website in frontend and backend scope with a dedicated API.

To delegate Content Security Policy handling to TYPO3, the scope-specific feature flags need to be enabled:

For new installations security.backend.enforceContentSecurityPolicy is enabled by default.

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,
        ),
    ),
]);
Copied!

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!

Modes

The following modes are available:

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
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
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
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
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
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: img-src 'self'
Copied!
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!

Reporting of violations

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, the reporting URL can be configured to use a third-party service as well:

// 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.

PSR-14 events

The following PSR-14 events are available: