vendor/nelmio/security-bundle/src/EventListener/ContentSecurityPolicyListener.php line 67

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Nelmio SecurityBundle.
  5.  *
  6.  * (c) Nelmio <hello@nelm.io>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Nelmio\SecurityBundle\EventListener;
  12. use Nelmio\SecurityBundle\ContentSecurityPolicy\DirectiveSet;
  13. use Nelmio\SecurityBundle\ContentSecurityPolicy\NonceGeneratorInterface;
  14. use Nelmio\SecurityBundle\ContentSecurityPolicy\ShaComputerInterface;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  18. use Symfony\Component\HttpKernel\KernelEvents;
  19. final class ContentSecurityPolicyListener extends AbstractContentTypeRestrictableListener
  20. {
  21.     private DirectiveSet $report;
  22.     private DirectiveSet $enforce;
  23.     private bool $compatHeaders;
  24.     /**
  25.      * @var list<string>
  26.      */
  27.     private array $hosts;
  28.     private ?string $_nonce null;
  29.     private ?string $scriptNonce null;
  30.     private ?string $styleNonce null;
  31.     /**
  32.      * @var array<string, list<string>>|null
  33.      */
  34.     private ?array $sha null;
  35.     private NonceGeneratorInterface $nonceGenerator;
  36.     private ShaComputerInterface $shaComputer;
  37.     /**
  38.      * @param list<string> $hosts
  39.      * @param list<string> $contentTypes
  40.      */
  41.     public function __construct(
  42.         DirectiveSet $report,
  43.         DirectiveSet $enforce,
  44.         NonceGeneratorInterface $nonceGenerator,
  45.         ShaComputerInterface $shaComputer,
  46.         bool $compatHeaders true,
  47.         array $hosts = [],
  48.         array $contentTypes = []
  49.     ) {
  50.         parent::__construct($contentTypes);
  51.         $this->report $report;
  52.         $this->enforce $enforce;
  53.         $this->compatHeaders $compatHeaders;
  54.         $this->hosts $hosts;
  55.         $this->nonceGenerator $nonceGenerator;
  56.         $this->shaComputer $shaComputer;
  57.     }
  58.     public function onKernelRequest(RequestEvent $e): void
  59.     {
  60.         if (!$e->isMainRequest()) {
  61.             return;
  62.         }
  63.         $this->sha = [];
  64.     }
  65.     public function addSha(string $directivestring $sha): void
  66.     {
  67.         if (null === $this->sha) {
  68.             // We're not in a request context, probably in a worker
  69.             // let's disable it to avoid memory leak
  70.             return;
  71.         }
  72.         $this->sha[$directive][] = $sha;
  73.     }
  74.     public function addScript(string $html): void
  75.     {
  76.         if (null === $this->sha) {
  77.             // We're not in a request context, probably in a worker
  78.             // let's disable it to avoid memory leak
  79.             return;
  80.         }
  81.         $this->sha['script-src'][] = $this->shaComputer->computeForScript($html);
  82.     }
  83.     public function addStyle(string $html): void
  84.     {
  85.         if (null === $this->sha) {
  86.             // We're not in a request context, probably in a worker
  87.             // let's disable it to avoid memory leak
  88.             return;
  89.         }
  90.         $this->sha['style-src'][] = $this->shaComputer->computeForStyle($html);
  91.     }
  92.     public function getReport(): DirectiveSet
  93.     {
  94.         return $this->report;
  95.     }
  96.     public function getEnforcement(): DirectiveSet
  97.     {
  98.         return $this->enforce;
  99.     }
  100.     public function getNonce(string $usage): string
  101.     {
  102.         $nonce $this->doGetNonce();
  103.         if ('script' === $usage) {
  104.             $this->scriptNonce $nonce;
  105.         } elseif ('style' === $usage) {
  106.             $this->styleNonce $nonce;
  107.         } else {
  108.             throw new \InvalidArgumentException('Invalid usage provided');
  109.         }
  110.         return $nonce;
  111.     }
  112.     public function onKernelResponse(ResponseEvent $e): void
  113.     {
  114.         if (!$e->isMainRequest()) {
  115.             return;
  116.         }
  117.         $request $e->getRequest();
  118.         $response $e->getResponse();
  119.         if ($response->isRedirection()) {
  120.             $this->_nonce null;
  121.             $this->styleNonce null;
  122.             $this->scriptNonce null;
  123.             $this->sha null;
  124.             return;
  125.         }
  126.         if (([] === $this->hosts || \in_array($e->getRequest()->getHost(), $this->hoststrue)) && $this->isContentTypeValid($response)) {
  127.             $signatures $this->sha;
  128.             if (null !== $this->scriptNonce) {
  129.                 $signatures['script-src'][] = 'nonce-'.$this->scriptNonce;
  130.             }
  131.             if (null !== $this->styleNonce) {
  132.                 $signatures['style-src'][] = 'nonce-'.$this->styleNonce;
  133.             }
  134.             if (!$response->headers->has('Content-Security-Policy-Report-Only')) {
  135.                 $response->headers->add($this->buildHeaders($request$this->reporttrue$this->compatHeaders$signatures));
  136.             }
  137.             if (!$response->headers->has('Content-Security-Policy')) {
  138.                 $response->headers->add($this->buildHeaders($request$this->enforcefalse$this->compatHeaders$signatures));
  139.             }
  140.         }
  141.         $this->_nonce null;
  142.         $this->styleNonce null;
  143.         $this->scriptNonce null;
  144.         $this->sha null;
  145.     }
  146.     public static function getSubscribedEvents(): array
  147.     {
  148.         return [
  149.             KernelEvents::REQUEST => ['onKernelRequest'512],
  150.             KernelEvents::RESPONSE => 'onKernelResponse',
  151.         ];
  152.     }
  153.     private function doGetNonce(): string
  154.     {
  155.         if (null === $this->_nonce) {
  156.             $this->_nonce $this->nonceGenerator->generate();
  157.         }
  158.         return $this->_nonce;
  159.     }
  160.     /**
  161.      * @param array<string, list<string>>|null $signatures
  162.      *
  163.      * @return array<string, string>
  164.      */
  165.     private function buildHeaders(
  166.         Request $request,
  167.         DirectiveSet $directiveSet,
  168.         bool $reportOnly,
  169.         bool $compatHeaders,
  170.         ?array $signatures null
  171.     ): array {
  172.         // $signatures might be null if no KernelEvents::REQUEST has been triggered.
  173.         // for instance if a security.authentication.failure has been dispatched
  174.         $headerValue $directiveSet->buildHeaderValue($request$signatures);
  175.         if ('' === $headerValue) {
  176.             return [];
  177.         }
  178.         $hn = static function (string $name) use ($reportOnly): string {
  179.             return $name.($reportOnly '-Report-Only' '');
  180.         };
  181.         $headers = [
  182.             $hn('Content-Security-Policy') => $headerValue,
  183.         ];
  184.         if ($compatHeaders) {
  185.             $headers[$hn('X-Content-Security-Policy')] = $headerValue;
  186.         }
  187.         return $headers;
  188.     }
  189. }