8397bdef4e50e5f25b1e48e6bd4bcc7ebfcbed00
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / RedirectResponseSubscriber.php
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
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;
16
17 /**
18  * Allows manipulation of the response object when performing a redirect.
19  */
20 class RedirectResponseSubscriber implements EventSubscriberInterface {
21
22   /**
23    * The unrouted URL assembler service.
24    *
25    * @var \Drupal\Core\Utility\UnroutedUrlAssemblerInterface
26    */
27   protected $unroutedUrlAssembler;
28
29   /**
30    * Constructs a RedirectResponseSubscriber object.
31    *
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.
36    */
37   public function __construct(UnroutedUrlAssemblerInterface $url_assembler, RequestContext $request_context) {
38     $this->unroutedUrlAssembler = $url_assembler;
39     $this->requestContext = $request_context;
40   }
41
42   /**
43    * Allows manipulation of the response object when performing a redirect.
44    *
45    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
46    *   The Event to process.
47    */
48   public function checkRedirectUrl(FilterResponseEvent $event) {
49     $response = $event->getResponse();
50     if ($response instanceof RedirectResponse) {
51       $request = $event->getRequest();
52
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');
57       if ($destination) {
58         // The 'Location' HTTP header must always be absolute.
59         $destination = $this->getDestinationAsAbsoluteUrl($destination, $request->getSchemeAndHttpHost());
60         try {
61           $response->setTargetUrl($destination);
62         }
63         catch (\InvalidArgumentException $e) {
64         }
65       }
66
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)) {
70         try {
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);
76         }
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
83           // server error.
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);
87         }
88         $event->setResponse($safe_response);
89       }
90     }
91   }
92
93   /**
94    * Converts the passed in destination into an absolute URL.
95    *
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.
101    *
102    * @return string
103    *   The destination as absolute URL.
104    */
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;
113       }
114       else {
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'];
119         $options = [
120           'query' => $destination['query'],
121           'fragment' => $destination['fragment'],
122           'absolute' => TRUE,
123         ];
124         // Treat this as if it's user input of a path relative to the site's
125         // base URL.
126         $destination = $this->unroutedUrlAssembler->assemble($uri, $options);
127       }
128     }
129     return $destination;
130   }
131
132   /**
133    * Sanitize the destination parameter to prevent open redirect attacks.
134    *
135    * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
136    *   The Event to process.
137    */
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');
153       }
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');
158       }
159     }
160   }
161
162   /**
163    * Registers the methods in this class that should be listeners.
164    *
165    * @return array
166    *   An array of event listener definitions.
167    */
168   public static function getSubscribedEvents() {
169     $events[KernelEvents::RESPONSE][] = ['checkRedirectUrl'];
170     $events[KernelEvents::REQUEST][] = ['sanitizeDestination', 100];
171     return $events;
172   }
173
174 }