vendor/nelmio/security-bundle/src/EventListener/ExternalRedirectListener.php line 69

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\ExternalRedirect\AllowListBasedTargetValidator;
  13. use Nelmio\SecurityBundle\ExternalRedirect\TargetValidator;
  14. use Psr\Log\LoggerInterface;
  15. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  16. use Symfony\Component\HttpKernel\Exception\HttpException;
  17. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  18. final class ExternalRedirectListener
  19. {
  20.     use KernelEventForwardCompatibilityTrait;
  21.     private bool $abort;
  22.     private ?string $override;
  23.     private ?string $forwardAs;
  24.     private ?TargetValidator $targetValidator;
  25.     private ?LoggerInterface $logger;
  26.     private ?UrlGeneratorInterface $generator;
  27.     /**
  28.      * @param bool                                     $abort           If true, the offending redirects are turned into 403 responses, can't be combined with $override
  29.      * @param string|null                              $override        Absolute path, complete URL or route name that must be used instead of the offending redirect's url
  30.      * @param string|null                              $forwardAs       Name of the route-/query string parameter the blocked url will be passed to destination location
  31.      * @param string|list<string>|TargetValidator|null $targetValidator array of hosts to be allowed, or regex that matches allowed hosts, or implementation of TargetValidator
  32.      * @param LoggerInterface|null                     $logger          A logger, if it's present, detected offenses are logged at the warning level
  33.      * @param UrlGeneratorInterface|null               $generator       Router or equivalent that can generate a route, only if override is a route name
  34.      */
  35.     public function __construct(
  36.         bool $abort true,
  37.         ?string $override null,
  38.         ?string $forwardAs null,
  39.         $targetValidator null,
  40.         ?LoggerInterface $logger null,
  41.         ?UrlGeneratorInterface $generator null
  42.     ) {
  43.         if (null !== $override && $abort) {
  44.             throw new \LogicException('The ExternalRedirectListener can not abort *and* override redirects at the same time.');
  45.         }
  46.         $this->abort $abort;
  47.         $this->override $override;
  48.         $this->forwardAs $forwardAs;
  49.         if (\is_string($targetValidator) || \is_array($targetValidator)) {
  50.             $targetValidator = new AllowListBasedTargetValidator($targetValidator);
  51.         } elseif (null !== $targetValidator && !$targetValidator instanceof TargetValidator) {
  52.             throw new \LogicException('$targetValidator should be an array of hosts, a regular expression, or an implementation of TargetValidator.');
  53.         }
  54.         $this->targetValidator $targetValidator;
  55.         $this->logger $logger;
  56.         $this->generator $generator;
  57.     }
  58.     public function onKernelResponse(ResponseEvent $e): void
  59.     {
  60.         if (!$this->isMainRequest($e)) {
  61.             return;
  62.         }
  63.         $response $e->getResponse();
  64.         if (!$response->isRedirect()) {
  65.             return;
  66.         }
  67.         $target $response->headers->get('Location');
  68.         if (null === $target) {
  69.             return;
  70.         }
  71.         if (!$this->isExternalRedirect($e->getRequest()->getUri(), $target)) {
  72.             return;
  73.         }
  74.         if (null !== $this->targetValidator && $this->targetValidator->isTargetAllowed($target)) {
  75.             return;
  76.         }
  77.         if (null !== $this->logger) {
  78.             $this->logger->warning('External redirect detected from '.$e->getRequest()->getUri().' to '.$response->headers->get('Location'));
  79.         }
  80.         if ($this->abort) {
  81.             throw new HttpException(403'Invalid Redirect Given: '.$response->headers->get('Location'));
  82.         }
  83.         if (null !== $this->override) {
  84.             $parameters = [];
  85.             if (null !== $this->forwardAs) {
  86.                 $parameters[$this->forwardAs] = $response->headers->get('Location');
  87.             }
  88.             if (false === strpos($this->override'/')) {
  89.                 if (null === $this->generator) {
  90.                     throw new \UnexpectedValueException('The listener needs a router/UrlGeneratorInterface object to override invalid redirects with routes');
  91.                 }
  92.                 $response->headers->set('Location'$this->generator->generate($this->override$parameters));
  93.             } else {
  94.                 $query '';
  95.                 if (\count($parameters) > 0) {
  96.                     $query = (false === strpos($this->override'?')) ? '?' '&';
  97.                     $query .= http_build_query($parameters'''&');
  98.                 }
  99.                 $response->headers->set('Location'$this->override.$query);
  100.             }
  101.         }
  102.     }
  103.     public function isExternalRedirect(string $sourcestring $target): bool
  104.     {
  105.         // cleanup "\rhttp://foo.com/" and other null prefixeds to be scanned as valid internal redirect
  106.         $target trim($target);
  107.         // handle protocol-relative URLs that parse_url() doesn't like
  108.         if ('//' === substr($target02)) {
  109.             $target 'proto:'.$target;
  110.         }
  111.         $parsedTarget parse_url($target);
  112.         if (false === $parsedTarget || !isset($parsedTarget['host'])) {
  113.             return false;
  114.         }
  115.         $parsedSource parse_url($source);
  116.         if (false === $parsedSource || !isset($parsedSource['host'])) {
  117.             throw new \LogicException('The source url must include a host name.');
  118.         }
  119.         return $parsedSource['host'] !== $parsedTarget['host'];
  120.     }
  121. }