This feature is available in Contao 5.3 and later.
Contao 5.3 introduced support for Content Security Policies (MDN Web Docs) for its front
end. Implementing CSP directives is an important tool to secure your website against XSS attacks and unwanted external
resources. At the same time we want to specifically allow scripts and resources generated or added by your application.
In this article we will show you how to use Contao’s CSP framework in order to adjust the generated
Content-Security-Policy
response header from within your templates, controllers and services.
CspHandler
Content Security Policies are applied via Contao’s Response Context concept. When CSP is enabled in
the settings of a website root a CspHandler
instance will be added to the response context. You can
access the CspHandler
in your services like this for example:
// src/ExampleService.php
namespace App;
use Contao\CoreBundle\Routing\ResponseContext\Csp\CspHandler;
use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor;
class ExampleService
{
public function __construct(private readonly ResponseContextAccessor $responseContextAccessor)
{
}
public function __invoke(): void
{
$responseContext = $this->responseContextAccessor->getResponseContext();
if ($responseContext?->has(CspHandler::class)) {
/** @var CspHandler $csp */
$cspHandler = $responseContext->get(CspHandler::class);
// Retrieve nonces, add sources, …
}
}
}
The following describes the most important methods of the CspHandler
. In many cases you will want to make these
adjustments directly from your templates though, rather than from within your controller for example. This way you can
provide a lot more flexibility to your users - if e.g. a part of the template is not output, it also does not need any
CSP information for it. Consequently Contao provides different template helper methods that you can use to manage CSP
nonces, sources and hashes.
Typically when employing CSP directives you want to only allow resources that come from the website itself, i.e. from
the same domain. This can be achieved via default-src 'self'
for example. If the website would then contain references
to an external source - e.g. via an <iframe>
or <script src="…">
etc. - that resource will be blocked by the
browser. However, your application might deliberately want to include external resources and thus it is necessary to
allow this resource specifically (or at least the domain of that resource). This can be done via the addSource
method:
$cspHandler->addSource('frame-src', 'https://www.youtube.com/embed/foobar123');
{% do csp_source('frame-src', 'https://www.youtube.com/embed/foobar123') %}
<?php $this->addCspSource('frame-src', 'https://www.youtube.com/embed/foobar123') ?>
The method will automatically not add the source, if no source list had been set yet for the given directive (or its
fallbacks). This behavior can be switched off via the $autoIgnore
parameter.
When employing a CSP directive that affects JavaScript or CSS (default-src
, script-src
, style-src
) inline scripts
will not be allowed anymore as it is recommended to only integrate scripts via files. However, if your application
still wants to use inline script or style elements it can still be allowed through the usage of nonces,
without having to resort to allowing 'unsafe-inline'
in your directives. Contao’s default JavaScript templates for
instance still use inline JavaScript and thus will add a nonce when used. You can retrieve a nonce for script-src
or
style-src
like this:
$nonce = $cspHandler->getNonce('script-src');
<script{{ attrs().setIfExists('nonce', csp_nonce('script-src')) }}>
<script<?= $this->attr()->setIfExists('nonce', $this->nonce('script-src')) ?>>
The method will automatically not return a nonce, if no source list had been set yet for the given directive (or its fallbacks).
This nonce can then be added as a nonce="…"
attribute to your <script>
or <style>
tag respectively.
As mentioned in the previous section, any directives that affect JavaScript or CSS will automatically disallow any
inline scripts or inline styles. This also includes inline scripts via HTML attributes, e.g. onclick="…"
or
style="…"
. It is highly recommended to move these styles and scripts into files instead. However, if for some reason
this is not possible such inline scripts can still be allowed through hashes. A hash can be generated and
added to a directive for a specific inline script or style like this:
$cspHandler->addHash('style-src', 'display:none');
{% do csp_hash('display:none') %}
<div style="display:none">
<div style="<?= $this->cspInlineStyle('display:none') ?>">
In order for inline styles to work in browsers that support CSP Level 3 you will also need to add 'unsafe-hashes'
to
the respective directive’s source list The same applies to inline JavaScripts for event listeners. See also these
examples for allowing
inline styles and
inline scripts. This is done automatically for you
when using the cspInlineStyle()
and cspInlineStyles()
helper method in PHP templates and the csp_inline_styles
filter in Twig templates (see also the WysiwygStyleProcessor
below).
WysiwygStyleProcessor
Contao uses the WYSIWYG Editor TinyMCE which creates inline styles for certain formatting options. In Contao’s default
templates these styles are automatically processed. The styles are extracted and filtered by the WysiwygStyleProcessor
and then their hashes are added via the CspHandler
. The allowed inline style properties can be configured via the
contao.csp.allowed_inline_styles
bundle configuration.
The 'unsafe-hashes'
directive will also be added automatically by the template helpers for CSP Level 3 compliance.
You can use the WysiwygStyleProcessor
directly in your services if you want to. Though typically you will likely only
need it in your templates (see other methods).
// src/ExampleService.php
namespace App;
use Contao\CoreBundle\Csp\WysiwygStyleProcessor;
use Contao\CoreBundle\Routing\ResponseContext\Csp\CspHandler;
use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor;
class ExampleService
{
public function __construct(
private readonly ResponseContextAccessor $responseContextAccessor,
private readonly WysiwygStyleProcessor $wysiwygProcessor,
) {
}
public function processInlineStyles(string $html): void
{
$responseContext = $this->responseContextAccessor->getResponseContext();
if (!$responseContext?->has(CspHandler::class)) {
return;
}
if (!$styles = $this->wysiwygProcessor->extractStyles($html)) {
return;
}
/** @var CspHandler $csp */
$csp = $responseContext->get(CspHandler::class);
foreach ($styles as $style) {
$csp->addHash('style-src', $style);
}
$csp->addSource('style-src', 'unsafe-hashes');
}
}
In Twig you can use the filter csp_inline_styles
. See the following example from Contao’s text.html.twig
template:
{{ text|csp_inline_styles|insert_tag|encode_email|raw }}
<?= $this->cspInlineStyles($this->text) ?>