vendor/nelmio/security-bundle/EventListener/ContentSecurityPolicyListener.php line 55

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