Newer
Older
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\RedirectResponseSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Component\HttpFoundation\SecuredRedirectResponse;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Routing\LocalRedirectResponse;
use Drupal\Core\Routing\RequestContext;
Angie Byron
committed
use Drupal\Core\Routing\UrlGeneratorInterface;
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 url generator service.
*
Angie Byron
committed
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Constructs a RedirectResponseSubscriber object.
*
Angie Byron
committed
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator service.
* @param \Drupal\Core\Routing\RequestContext $request_context
* The request context.
*/
public function __construct(UrlGeneratorInterface $url_generator, RequestContext $request_context) {
$this->urlGenerator = $url_generator;
$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);
}
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 relative paths that have
// not yet been converted to URLs (outbound path processors and other
// URL handling still needs to be performed).
// @todo As generateFromPath() is deprecated, remove this in
// https://www.drupal.org/node/2418219.
$destination = UrlHelper::parse($destination);
$path = $destination['path'];
$options = [
'query' => $destination['query'],
'fragment' => $destination['fragment'],
'absolute' => TRUE,
];
$destination = $this->urlGenerator->generateFromPath($path, $options);
}
}
return $destination;
}
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/**
* 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.
*/
static function getSubscribedEvents() {
$events[KernelEvents::RESPONSE][] = array('checkRedirectUrl');
$events[KernelEvents::REQUEST][] = array('sanitizeDestination', 100);
return $events;
}
}