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

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