3 namespace Drupal\Core\EventSubscriber;
5 use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
6 use Drupal\Component\Utility\UrlHelper;
7 use Drupal\Core\Routing\LocalRedirectResponse;
8 use Drupal\Core\Routing\RequestContext;
9 use Drupal\Core\Utility\UnroutedUrlAssemblerInterface;
10 use Symfony\Component\HttpFoundation\Response;
11 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
12 use Symfony\Component\HttpKernel\KernelEvents;
13 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
14 use Symfony\Component\HttpFoundation\RedirectResponse;
15 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
18 * Allows manipulation of the response object when performing a redirect.
20 class RedirectResponseSubscriber implements EventSubscriberInterface {
23 * The unrouted URL assembler service.
25 * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
27 protected $unroutedUrlAssembler;
30 * Constructs a RedirectResponseSubscriber object.
32 * @param \Drupal\Core\Utility\UnroutedUrlAssemblerInterface $url_assembler
33 * The unrouted URL assembler service.
34 * @param \Drupal\Core\Routing\RequestContext $request_context
35 * The request context.
37 public function __construct(UnroutedUrlAssemblerInterface $url_assembler, RequestContext $request_context) {
38 $this->unroutedUrlAssembler = $url_assembler;
39 $this->requestContext = $request_context;
43 * Allows manipulation of the response object when performing a redirect.
45 * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
46 * The Event to process.
48 public function checkRedirectUrl(FilterResponseEvent $event) {
49 $response = $event->getResponse();
50 if ($response instanceof RedirectResponse) {
51 $request = $event->getRequest();
53 // Let the 'destination' query parameter override the redirect target.
54 // If $response is already a SecuredRedirectResponse, it might reject the
55 // new target as invalid, in which case proceed with the old target.
56 $destination = $request->query->get('destination');
58 // The 'Location' HTTP header must always be absolute.
59 $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost());
61 $response->setTargetUrl($destination);
63 catch (\InvalidArgumentException $e) {
67 // Regardless of whether the target is the original one or the overridden
68 // destination, ensure that all redirects are safe.
69 if (!($response instanceof SecuredRedirectResponse)) {
71 // SecuredRedirectResponse is an abstract class that requires a
72 // concrete implementation. Default to LocalRedirectResponse, which
73 // considers only redirects to within the same site as safe.
74 $safe_response = LocalRedirectResponse::createFromRedirectResponse($response);
75 $safe_response->setRequestContext($this->requestContext);
77 catch (\InvalidArgumentException $e) {
78 // If the above failed, it's because the redirect target wasn't
79 // local. Do not follow that redirect. Display an error message
80 // instead. We're already catching one exception, so trigger_error()
81 // rather than throw another one.
82 // We don't throw an exception, because this is a client error rather than a
84 $message = 'Redirects to external URLs are not allowed by default, use \Drupal\Core\Routing\TrustedRedirectResponse for it.';
85 trigger_error($message, E_USER_ERROR);
86 $safe_response = new Response($message, 400);
88 $event->setResponse($safe_response);
94 * Converts the passed in destination into an absolute URL.
96 * @param string $destination
97 * The path for the destination. In case it starts with a slash it should
98 * have the base path included already.
99 * @param string $scheme_and_host
100 * The scheme and host string of the current request.
103 * The destination as absolute URL.
105 protected function getDestinationAsAbsoluteUrl($destination, $scheme_and_host) {
106 if (!UrlHelper::isExternal($destination)) {
107 // The destination query parameter can be a relative URL in the sense of
108 // not including the scheme and host, but its path is expected to be
109 // absolute (start with a '/'). For such a case, prepend the scheme and
110 // host, because the 'Location' header must be absolute.
111 if (strpos($destination, '/') === 0) {
112 $destination = $scheme_and_host . $destination;
115 // Legacy destination query parameters can be internal paths that have
116 // not yet been converted to URLs.
117 $destination = UrlHelper::parse($destination);
118 $uri = 'base:' . $destination['path'];
120 'query' => $destination['query'],
121 'fragment' => $destination['fragment'],
124 // Treat this as if it's user input of a path relative to the site's
126 $destination = $this->unroutedUrlAssembler->assemble($uri, $options);
133 * Sanitize the destination parameter to prevent open redirect attacks.
135 * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
136 * The Event to process.
138 public function sanitizeDestination(GetResponseEvent $event) {
139 $request = $event->getRequest();
140 // Sanitize the destination parameter (which is often used for redirects) to
141 // prevent open redirect attacks leading to other domains. Sanitize both
142 // $_GET['destination'] and $_REQUEST['destination'] to protect code that
143 // relies on either, but do not sanitize $_POST to avoid interfering with
144 // unrelated form submissions. The sanitization happens here because
145 // url_is_external() requires the variable system to be available.
146 $query_info = $request->query;
147 $request_info = $request->request;
148 if ($query_info->has('destination') || $request_info->has('destination')) {
149 // If the destination is an external URL, remove it.
150 if ($query_info->has('destination') && UrlHelper::isExternal($query_info->get('destination'))) {
151 $query_info->remove('destination');
152 $request_info->remove('destination');
154 // If there's still something in $_REQUEST['destination'] that didn't come
155 // from $_GET, check it too.
156 if ($request_info->has('destination') && (!$query_info->has('destination') || $request_info->get('destination') != $query_info->get('destination')) && UrlHelper::isExternal($request_info->get('destination'))) {
157 $request_info->remove('destination');
163 * Registers the methods in this class that should be listeners.
166 * An array of event listener definitions.
168 public static function getSubscribedEvents() {
169 $events[KernelEvents::RESPONSE][] = ['checkRedirectUrl'];
170 $events[KernelEvents::REQUEST][] = ['sanitizeDestination', 100];