Ever fought with CSP headers? Me too. It always used to be a pain to configure CSP headers properly.
But setting CSP header directives is more important than ever! If you ever came across different tracking scripts, you probably also noticed how many additional fourth-party scripts are lazy loaded. This could lead to malicious JavaScript being loaded to your page, which could be catastrophic, especially when building payment gateways.
It even helps you with adding dynamic Nonce-Tokens when not using the unsafe-inline
directive (which you should avoid)
- PHP >= 7.4.33 with OpenSSL extension installed
- Symfony >= 5.4
composer require opctim/symfony-csp-bundle
In your config/
directory, add / edit opctim_csp_bundle.yaml
:
# config/packages/opctim_csp_bundle.yaml
opctim_csp_bundle:
always_add: []
report:
url: null
route: null
route_params: []
chance: 100
directives:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
base-uri:
- "'self'"
object-src:
- "'none'"
script-src:
- "'self'"
- "nonce(payment-app)" # For more info, see "Dynamic nonce tokens" section below!
- '*.example.com'
img-src:
- "'self'"
- '*.example.com'
style-src:
- "'self'"
- "'unsafe-inline'"
connect-src:
- '*.example.com'
font-src:
- '*.example.com'
frame-src:
- "'self'"
- '*.example.com'
frame-ancestors:
- "'self'"
- '*.example.com'
You can use any directives you want here! This is just a fancy way of writing the directives.
So:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
becomes
Content-Security-Policy: default-src 'self' data: *.example.com;
As the name implies, this option adds the specified origins to all directives. This can be useful with when@dev
:
# config/packages/opctim_csp_bundle.yaml
opctim_csp_bundle:
always_add: []
directives:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
base-uri:
- "'self'"
object-src:
- "'none'"
script-src:
- "'self'"
- "nonce(payment-app)" # For more info, see "Dynamic nonce tokens" section below!
- '*.example.com'
when@dev:
opctim_csp_bundle:
always_add:
- '*.example.local'
Important: If you add 'none'
as the first and only directive, this directive will be skipped for the always_add
functionality. This feature was added in 1.1.4
You also can use when@dev
to add origins to specific directives conditionally:
# config/packages/opctim_csp_bundle.yaml
opctim_csp_bundle:
always_add: []
directives:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
script-src:
- "'self'"
- '*.example.com'
when@dev:
opctim_csp_bundle:
directives:
connect-src:
- 'some.external.additional.host.com'
This bundle provides you with an easy way to configure the report feature of CSP, which tells browsers to tell your backend if your CSP configuration denies specific resources. There are currently two implementations in browsers - report-uri & report-to:
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
So, according to the MDN docs, this bundle adds the report-uri directive & the Reporting-Endpoint header to support new Browsers in the future.
This bundle provides a backwards compatible implementation, which should be supported by all browsers.
# config/packages/opctim_csp_bundle.yaml
opctim_csp_bundle:
always_add: []
report:
url: null
route: my_awesome_controller_action
route_params: []
chance: 100
directives:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
url
-optional
You can pass an external URL here, which the browsers should report to.route
-optional
If you want to use your controller action to receive reports. This will use the UrlGenerator to generate an absolute url for you.route_params
-optional
You can pass additional route parameters here, if you're using theroute
parameter.chance
-optional
This fields' unit is percent. It specifies how high the chance should be to add the report directives to the response.
Here is some pseudocode explaining the change option:
if (random_int(0, 99) < $chance) {
$someService->addReportHeaders();
}
This means, that for a chance of 100%, it will run every time. Depending on traffic of your app, it is recommended to set a chance of around 5-10%, to not get flooded by CSP log messages.
Dynamic nonce tokens can be extremely useful, to allow specific inline script tags in your Twig templates, without having to ignore security concerns, e.g. by not adding or hard-coding them ;)
nonce(<handle>)
In opctim_csp_bundle.yaml
:
opctim_csp_bundle:
always_add: []
directives:
default-src:
- "'self'"
- 'data:'
- '*.example.com'
script-src:
- "'self'"
- '*.example.com'
- 'nonce(my-inline-script)'
On request, nonce(my-inline-script)
will be transformed to e.g. nonce-25d2ec8bb6
and will later appear in the response CSP header.
Then, in your twig template you can simply use the csp_nonce('my-inline-script')
function that is provided by this bundle:
<script type="text/javascript" nonce="{{ csp_nonce('my-inline-script') }}">
alert('This works!');
</script>
The rendered result:
<script type="text/javascript" nonce="25d2ec8bb6">
alert('This works!');
</script>
A key feature of this bundle is the dynamic nonce implementation. The bundle hooks into the Symfony event system and generates fresh nonce tokens for you - on every request!
On request, the bundle prepares the CSP header directives to be written to headers on response.
Here, the nonce()
expressions from opctim_csp_bundle.yaml
are parsed.
The bundle will add this value to the Response in the following three headers for compatibility across browsers:
- Content-Security-Policy
- X-Content-Security-Policy
- X-WebKit-CSP
If you want to modify the CSP header before it is written to the response,
you can hook into the generation by subscribing to the opctim_csp_bundle.add_csp_header
event:
<?php # src/EventSubscriber/ModifyCspHeaderEventSubscriber.php
declare(strict_types=1);
namespace App\EventSubscriber;
use Opctim\CspBundle\Event\AddCspHeaderEvent;
use Opctim\CspBundle\Service\CspHeaderBuilderService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ModifyCspHeaderEventSubscriber implements EventSubscriberInterface
{
public function __construct(
private CspHeaderBuilderService $cspHeaderBuilderService
)
{}
public static function getSubscribedEvents(): array
{
return [
AddCspHeaderEvent::NAME => 'modifyCspHeader'
];
}
public function modifyCspHeader(AddCspHeaderEvent $event): void
{
// Use the request if you like
$request = $event->getRequest();
$cspHeader = $this->cspHeaderBuilderService->build(
[ // alwaysAdd options
...$this->cspHeaderBuilderService->getAlwaysAdd(), // Merge the existing ones...
'some-conditional-always-to-be-added-origin'
],
[ // directive options
...$this->cspHeaderBuilderService->getDirectives(), // Merge the existing ones...
'script-src' => [ // Override something here
'some-conditional-origin'
]
]
);
$cspHeader = str_replace('foo', 'bar', $cspHeader); // Maybe some string transformations here...
$event->setCspHeaderValue($cspHeader); // Set the newly crafted csp header.
// On response, the bundle will add your new CSP header!
}
}
Tests are located inside the tests/
folder and can be run with vendor/bin/phpunit
:
composer install
vendor/bin/phpunit