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.
See also
If you are not familiar with Content Security Policy, please read the following resources:
- Introduction to Content Security Policy (CSP)
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
At the TYPO3 Developer Days 2023 Oliver Hader talked about Content Security Policy in general and in TYPO3:
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 frontend, the feature flag security.frontend.enforceContentSecurityPolicy needs to be enabled.
Changed in version 13.0
In TYPO3 backend the Content Security Policy is always enforced.
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));
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:
Note
The policy builder is the low-level representation and interaction in PHP,
any other configuration is using the same verbs to describe the CSP
instructions. The \TYPO3\
object is used for compiling the CSP in a middleware.
Thus, custom controllers or middlewares could use this approach; the last
line
header('Content-Security-Policy: ' . $policy->compile($nonce));
is an example to show the basic principle without having to explain PSR-7/PSR-15 details.
For project integrations the "mutations" (via configuration, YAML, resolutions in the UI or events) shall be used.
Extension-specific¶
Policies for frontend and backend can be applied automatically by providing a
Configuration/
file in an extension, for
example:
<?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,
),
),
]);
Site-specific (frontend)¶
In frontend, a dedicated sites/<my_
can be
used to declare policies for a specific site, for example:
# 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:'
Modes¶
The following modes are available:
-
append
¶ -
- YAML
append
- PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Append
Appends to a given directive.
Example:
mutations: - mode: set directive: "default-src" sources: - "'self'" - mode: set directive: "img-src" sources: - example.org - mode: append directive: "img-src" sources: - example.com
<?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'), ), ), ]);
Results in:
Content-Security-Policy: default-src 'self'; img-src example.org example.com
Copied!
-
extend
¶ -
- YAML
extend
- PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Extend
Extends the given directive. It is a shortcut for inherit-once and append.
Example:
mutations: - mode: set directive: "default-src" sources: - "'self'" - mode: extend directive: "img-src" sources: - "example.com"
<?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'), ), ), ]);
Results in:
Content-Security-Policy: default-src 'self'; img-src 'self' example.com
Copied!
-
inherit-
¶again -
- YAML
inherit-
again - PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Inherit Again
Inherits again from the corresponding ancestor chain and merges existing sources.
Example:
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"
<?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, ), ), ]);
Results in:
Content-Security-Policy: default-src data:; img-src data: 'self' example.com
Copied!Note that
data:
is inherited toimg-
(in opposite to inherit-once).src
-
inherit-
¶once -
- YAML
inherit-
once - PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Inherit Once
Inherits once from the corresponding ancestor chain. When
inherit-
is called multiple times on the same directive, only the first time is applied.once Example:
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"
<?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, ), ), ]);
Results in:
Content-Security-Policy: default-src data:; img-src 'self' example.com
Copied!Note that
data:
is not inherited toimg-
. If you want to inherit alsosrc data:
toimg-
use inherit-again.src
-
reduce
¶ -
- YAML
reduce
- PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Reduce
Reduces a directive by a given aspect.
Example:
mutations: - mode: set directive: "img-src" sources: - "'self'" - "data:" - "example.com" - mode: reduce directive: "img-src" sources: - "data:"
<?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, ), ), ]);
Results in:
Content-Security-Policy: default-src 'self' example.com
Copied!
-
remove
¶ -
- YAML
remove
- PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Remove
Removes a directive completely.
Example:
mutations: - mode: set directive: "default-src" sources: - "'self'" - mode: set directive: "img-src" sources: - "data:" - mode: remove directive: "img-src"
<?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, ), ), ]);
Results in:
Content-Security-Policy: img-src 'self'
Copied!
-
set
¶ -
- YAML
set
- PHP
\TYPO3\
CMS\ Core\ Security\ Content Security Policy\ Mutation Mode:: Set
Sets (overrides) a directive completely.
Example:
<?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, ), ), ]);
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>
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.
, 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();
}
In a Fluid template¶
The f:security.nonce ViewHelper is available, which provides the nonce in a Fluid template, for example:
<script nonce="{f:security.nonce()}">
const inline = 'script';
</script>
<style nonce="{f:security.nonce()}">
.some-style { color: red; }
</style>
You can also use the f:asset.script
or f:asset.css
ViewHelpers with the use
attribute:
<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>
Reporting of violations¶
Potential CSP violations are reported back to the TYPO3 system and persisted
internally in the database table sys_
. 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_
) which
extends the Content Security Policy for the given scope during runtime:
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.
Note
If you apply the suggestion, it is stored in the database table
sys_
. To have all policies in one place, you
should consider adding the suggestion to your
extension-specific or
site-specific CSP definitions
manually.
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/';
Violations are then sent to the third-party service instead of the TYPO3 endpoint.
PSR-14 events¶
The following PSR-14 events are available: