NelmioSecurityBundle
NelmioSecurityBundle copied to clipboard
Introduce `DirectiveSetBuilderInterface` to allow runtime modification
This PR introduces the DirectiveSetBuilderInterface and the default implementation ConfigurationDirectiveSetBuilder proposed in #347, to allow for runtime modification of the CSP directive sets. The major changes are:
- Introduction of
DirectiveSetBuilderInterfaceandConfigurationDirectiveSetBuilder; - The
ContentSecurityPolicyListenerconstructor now takesDirectiveSetBuilderInterfaceinstead of the directive sets directly; - Changes to
NelmioSecurityExtensionto provide the configuration to theConfigurationDirectiveSetBuilders, which in turn are injected intoContentSecurityPolicyListener(instead of the directive sets).
This adds a layer between the configuration (and the directive sets built from it) and ContentSecurityPolicyListener. This layer provides an integration point for application code to modify the directive sets based on the request (e.g., in a controller or a kernel event listener).
Sorry but I won't manage to review this one right now, seems solid at first sight but I'd rather give it some more thought.
@Seldaek Any chance this can be looked at? Thanks in advance!
Thanks - what would be great is if someone can provide some code sample for the docs on how this can be used to configure extra values
I now implemented my CSP exceptions like this:
public function page(ContentSecurityPolicyListener $cspListener) {
// Allow *.doubleclick.net as extra script-src
$cspListener->getEnforcement()->setDirective('script-src', $cspListener->getEnforcement()->getDirective('script-src') . ' *.doubleclick.net');
return $this->render('exampe.html.twig');
}
ContentSecurityPolicyListener::getEnforcement() is now deprecated.
Can an example be provided how to achieve the same behaviour in the new way?
The idea is that your app or bundle can now provide its own DirectiveSetBuilderInterface which builds or modifies the directive set. Your implementation can replace or decorate nelmio_security.directive_set_builder.enforce. @.inner would be the default implementation (ConfigurationDirectiveSetBuilder), which holds the directive set that is build from the config. For your case, you could do something like this:
<?php
declare(strict_types=1);
namespace App;
use Nelmio\SecurityBundle\ContentSecurityPolicy\DirectiveSet;
use Nelmio\SecurityBundle\ContentSecurityPolicy\DirectiveSetBuilderInterface;
class EnforcedDirectiveSetBuilder implements DirectiveSetBuilderInterface
{
private DirectiveSet $enforcedSet;
public function __construct(
DirectiveSetBuilderInterface $decorated,
) {
// Get the directive set build from the config.
$this->enforcedSet = $decorated->buildDirectiveSet();
}
public function buildDirectiveSet(): DirectiveSet
{
return $this->enforcedSet;
}
// or `addScriptSrc`, ....
public function getDirectiveSet(): DirectiveSet
{
return $this->enforcedSet;
}
}
And register it in the container
App\EnforcedDirectiveSetBuilder:
decorates: 'nelmio_security.directive_set_builder.enforce'
arguments:
- '@.inner'
In your controller you can then inject EnforcedDirectiveSetBuilder instead and do
$cspEnforcement = $this->enforcedDirectiveSetBuilder->getDirectiveSet();
$cspEnforcement->setDirective('script-src', $cspEnforcement->getDirective('script-src') . ' https://*.some');
The EnforcedDirectiveSetBuilder can do a lot more; you can add methods like addScriptSrc or inject the RequestStack and modify based on the request URI, the matched route, ...
I will look at creating a PR to add some examples to the documentation.