opctim/symfony-csp-bundle

This bundle helps to properly secure your application using the CSP header in a symfony application.

Installs: 60

Dependents: 0

Suggesters: 0

Security: 0

Stars: 1

Watchers: 1

Forks: 0

Open Issues: 0

Type:symfony-bundle

1.1.3 2024-04-25 17:55 UTC

This package is auto-updated.

Last update: 2024-04-30 18:33:34 UTC


README

Latest Stable Version Total Downloads Latest Unstable Version License PHP Version Require

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)

Requirements

  • PHP >= 7.4.33 with OpenSSL extension installed
  • Symfony >= 5.4

Installation

composer require opctim/symfony-csp-bundle

Configuration

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;

The always_add option

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'

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'

The report option

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:

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 the route 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

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 ;)

Configuration syntax

nonce(<handle>)

Example

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>

Hooking into the CSP header generation

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

Tests are located inside the tests/ folder and can be run with vendor/bin/phpunit:

composer install

vendor/bin/phpunit