Feature: #99499 - Introduce Content-Security-Policy Handling

See forge#99499

Description

Corresponding representation of the W3C standard of Content-Security-Policy (CSP) has been introduced to TYPO3. Content-Security-Policy declarations can either be provided by using the the general builder pattern of \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Policy, extension specific mutations (changes to the general policy) via Configuration/ContentSecurityPolicies.php located in corresponding extension directories, or YAML path contentSecurityPolicies.mutations for site-specific declarations in the website frontend.

The PSR-15 middlewares ContentSecurityPolicyHeaders are applying Content-Security-Policy HTTP headers to each response in the frontend and backend scope. In case other components have already added either the header Content-Security-Policy or Content-Security-Policy-Report-Only, those existing headers will be kept without any modification - these events will be logged with an info severity.

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

  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.backend.enforceContentSecurityPolicy']

  • $GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['security.frontend.enforceContentSecurityPolicy']

For new installations security.backend.enforceContentSecurityPolicy is enabled via factory default settings.

Impact

Introducing CSP to TYPO3 aims to reduces the risk of being affected by Cross-Site-Scripting due to the lack of proper encoding of user-submitted content in corresponding outputs.

Configuration

Policy builder approach

<?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($nonce))
    // 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: ' . (string)$policy);

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:

Extension specific

Having a file Configuration/ContentSecurityPolicies.php in the base directory of any extension declared, will automatically provide and apply corresponding settings.

<?php
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')),
        // 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),
    ),
]);

Site-specific (frontend)

In the frontend, the dedicated sites/my-site/csp.yaml can be used to declare CSP for a specific site as well.

# 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:'

PSR-14 Events

The \TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\PolicyMutatedEvent will be dispatched once all mutations have been applied to the current policy object, just before the corresponding HTTP header is added to the HTTP response object. This allows individual adjustments for custom implementations.