123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- <?php
- namespace Drupal\Core\EventSubscriber;
- use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
- use Drupal\Component\Utility\UrlHelper;
- use Drupal\Core\Routing\LocalRedirectResponse;
- use Drupal\Core\Routing\RequestContext;
- use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
- use Symfony\Component\HttpFoundation\Response;
- use Symfony\Component\HttpKernel\Event\GetResponseEvent;
- use Symfony\Component\HttpKernel\KernelEvents;
- use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
- use Symfony\Component\HttpFoundation\RedirectResponse;
- use Symfony\Component\EventDispatcher\EventSubscriberInterface;
- /**
- * Allows manipulation of the response object when performing a redirect.
- */
- class RedirectResponseSubscriber implements EventSubscriberInterface {
- /**
- * The unrouted URL assembler service.
- *
- * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
- */
- protected $unroutedUrlAssembler;
- /**
- * Constructs a RedirectResponseSubscriber object.
- *
- * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface $url_assembler
- * The unrouted URL assembler service.
- * @param \Drupal\Core\Routing\RequestContext $request_context
- * The request context.
- */
- public function __construct(UnroutedUrlAssemblerInterface $url_assembler, RequestContext $request_context) {
- $this->unroutedUrlAssembler = $url_assembler;
- $this->requestContext = $request_context;
- }
- /**
- * Allows manipulation of the response object when performing a redirect.
- *
- * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
- * The Event to process.
- */
- public function checkRedirectUrl(FilterResponseEvent $event) {
- $response = $event->getResponse();
- if ($response instanceof RedirectResponse) {
- $request = $event->getRequest();
- // Let the 'destination' query parameter override the redirect target.
- // If $response is already a SecuredRedirectResponse, it might reject the
- // new target as invalid, in which case proceed with the old target.
- $destination = $request->query->get('destination');
- if ($destination) {
- // The 'Location' HTTP header must always be absolute.
- $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost());
- try {
- $response->setTargetUrl($destination);
- }
- catch (\InvalidArgumentException $e) {
- }
- }
- // Regardless of whether the target is the original one or the overridden
- // destination, ensure that all redirects are safe.
- if (!($response instanceof SecuredRedirectResponse)) {
- try {
- // SecuredRedirectResponse is an abstract class that requires a
- // concrete implementation. Default to LocalRedirectResponse, which
- // considers only redirects to within the same site as safe.
- $safe_response = LocalRedirectResponse::createFromRedirectResponse($response);
- $safe_response->setRequestContext($this->requestContext);
- }
- catch (\InvalidArgumentException $e) {
- // If the above failed, it's because the redirect target wasn't
- // local. Do not follow that redirect. Display an error message
- // instead. We're already catching one exception, so trigger_error()
- // rather than throw another one.
- // We don't throw an exception, because this is a client error rather than a
- // server error.
- $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.';
- trigger_error($message, E_USER_ERROR);
- $safe_response = new Response($message, 400);
- }
- $event->setResponse($safe_response);
- }
- }
- }
- /**
- * Converts the passed in destination into an absolute URL.
- *
- * @param string $destination
- * The path for the destination. In case it starts with a slash it should
- * have the base path included already.
- * @param string $scheme_and_host
- * The scheme and host string of the current request.
- *
- * @return string
- * The destination as absolute URL.
- */
- protected function getDestinationAsAbsoluteUrl($destination, $scheme_and_host) {
- if (!UrlHelper::isExternal($destination)) {
- // The destination query parameter can be a relative URL in the sense of
- // not including the scheme and host, but its path is expected to be
- // absolute (start with a '/'). For such a case, prepend the scheme and
- // host, because the 'Location' header must be absolute.
- if (strpos($destination, '/') === 0) {
- $destination = $scheme_and_host . $destination;
- }
- else {
- // Legacy destination query parameters can be internal paths that have
- // not yet been converted to URLs.
- $destination = UrlHelper::parse($destination);
- $uri = 'base:' . $destination['path'];
- $options = [
- 'query' => $destination['query'],
- 'fragment' => $destination['fragment'],
- 'absolute' => TRUE,
- ];
- // Treat this as if it's user input of a path relative to the site's
- // base URL.
- $destination = $this->unroutedUrlAssembler->assemble($uri, $options);
- }
- }
- return $destination;
- }
- /**
- * Sanitize the destination parameter to prevent open redirect attacks.
- *
- * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
- * The Event to process.
- */
- public function sanitizeDestination(GetResponseEvent $event) {
- $request = $event->getRequest();
- // Sanitize the destination parameter (which is often used for redirects) to
- // prevent open redirect attacks leading to other domains. Sanitize both
- // $_GET['destination'] and $_REQUEST['destination'] to protect code that
- // relies on either, but do not sanitize $_POST to avoid interfering with
- // unrelated form submissions. The sanitization happens here because
- // url_is_external() requires the variable system to be available.
- $query_info = $request->query;
- $request_info = $request->request;
- if ($query_info->has('destination') || $request_info->has('destination')) {
- // If the destination is an external URL, remove it.
- if ($query_info->has('destination') && UrlHelper::isExternal($query_info->get('destination'))) {
- $query_info->remove('destination');
- $request_info->remove('destination');
- }
- // If there's still something in $_REQUEST['destination'] that didn't come
- // from $_GET, check it too.
- if ($request_info->has('destination') && (!$query_info->has('destination') || $request_info->get('destination') != $query_info->get('destination')) && UrlHelper::isExternal($request_info->get('destination'))) {
- $request_info->remove('destination');
- }
- }
- }
- /**
- * Registers the methods in this class that should be listeners.
- *
- * @return array
- * An array of event listener definitions.
- */
- public static function getSubscribedEvents() {
- $events[KernelEvents::RESPONSE][] = ['checkRedirectUrl'];
- $events[KernelEvents::REQUEST][] = ['sanitizeDestination', 100];
- return $events;
- }
- }
|