diff --git a/CHANGELOG.md b/CHANGELOG.md index af82bb5..f72b4a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### 3.x.x (xxxx-xx-xx) +* Added `csp > request_matcher` option to allow the use of a custom request matcher (`Symfony\Component\HttpFoundation\RequestMatcherInterface`) ### 3.1.0 (2023-12-03) * Fixed overriding CSP header diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index cc6b56e..0bae424 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -81,6 +81,7 @@ private function addCspNode(): ArrayNodeDefinition ->canBeDisabled() // CSP is enabled by default to ensure BC ->children() + ->scalarNode('request_matcher')->defaultNull()->end() ->arrayNode('hosts')->scalarPrototype()->end()->defaultValue([])->end() ->arrayNode('content_types')->scalarPrototype()->end()->defaultValue([])->end() ->arrayNode('report_endpoint') diff --git a/src/DependencyInjection/NelmioSecurityExtension.php b/src/DependencyInjection/NelmioSecurityExtension.php index 580a6ca..8e73f67 100644 --- a/src/DependencyInjection/NelmioSecurityExtension.php +++ b/src/DependencyInjection/NelmioSecurityExtension.php @@ -57,6 +57,10 @@ public function load(array $configs, ContainerBuilder $container): void $cspListenerDefinition->setArguments([$reportDefinition, $enforceDefinition, new Reference('nelmio_security.nonce_generator'), new Reference('nelmio_security.sha_computer'), (bool) $cspConfig['compat_headers'], $cspConfig['hosts'], $cspConfig['content_types']]); $container->setParameter('nelmio_security.csp.hash_algorithm', $cspConfig['hash']['algorithm']); + if (isset($cspConfig['request_matcher'])) { + $cspListenerDefinition->setArgument(7, new Reference($cspConfig['request_matcher'])); + } + $cspViolationLogFilterDefinition = $container->getDefinition('nelmio_security.csp_report.filter'); $container->setParameter('nelmio_security.csp.report_log_level', $cspConfig['report_endpoint']['log_level']); diff --git a/src/EventListener/ContentSecurityPolicyListener.php b/src/EventListener/ContentSecurityPolicyListener.php index a1c13de..df5833f 100644 --- a/src/EventListener/ContentSecurityPolicyListener.php +++ b/src/EventListener/ContentSecurityPolicyListener.php @@ -17,6 +17,7 @@ use Nelmio\SecurityBundle\ContentSecurityPolicy\NonceGeneratorInterface; use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -41,6 +42,7 @@ final class ContentSecurityPolicyListener extends AbstractContentTypeRestrictabl private ?array $sha = null; private NonceGeneratorInterface $nonceGenerator; private ShaComputerInterface $shaComputer; + private ?RequestMatcherInterface $requestMatcher; /** * @param list $hosts @@ -53,7 +55,8 @@ public function __construct( ShaComputerInterface $shaComputer, bool $compatHeaders = true, array $hosts = [], - array $contentTypes = [] + array $contentTypes = [], + ?RequestMatcherInterface $requestMatcher = null ) { parent::__construct($contentTypes); $this->report = $report; @@ -62,6 +65,7 @@ public function __construct( $this->hosts = $hosts; $this->nonceGenerator = $nonceGenerator; $this->shaComputer = $shaComputer; + $this->requestMatcher = $requestMatcher; } public function onKernelRequest(RequestEvent $e): void @@ -149,7 +153,13 @@ public function onKernelResponse(ResponseEvent $e): void return; } - if (([] === $this->hosts || \in_array($e->getRequest()->getHost(), $this->hosts, true)) && $this->isContentTypeValid($response)) { + if ($this->requestMatcher) { + $match = $this->requestMatcher->matches($request); + } else { + $match = ([] === $this->hosts || \in_array($e->getRequest()->getHost(), $this->hosts, true)) && $this->isContentTypeValid($response); + } + + if ($match) { $signatures = $this->sha; if (null !== $this->scriptNonce) { $signatures['script-src'][] = 'nonce-'.$this->scriptNonce; diff --git a/src/Resources/doc/index.rst b/src/Resources/doc/index.rst index 74d9e84..ed1e3b3 100644 --- a/src/Resources/doc/index.rst +++ b/src/Resources/doc/index.rst @@ -204,6 +204,10 @@ Finally, an optional ``hosts`` key lets you configure which hostnames (e.g. ``fo the CSP rule should be enforced on. If the list is empty (it is by default), all hostnames will use the CSP rule. +If the `content_types` and `hosts` options don’t fit your needs, you can also configure a service implementing +`Symfony\Component\HttpFoundation\RequestMatcherInterface` as `request_matcher`. Then the `content_types` and `hosts` +options are no longer used. + .. code-block:: yaml # config/packages/nelmio_security.yaml @@ -211,6 +215,7 @@ hostnames will use the CSP rule. csp: enabled: true report_logger_service: logger + request_matcher: null hosts: [] content_types: [] enforce: