DomainRedirectResponse.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. <?php
  2. namespace Drupal\domain;
  3. use Drupal\Core\Routing\CacheableSecuredRedirectResponse;
  4. use Drupal\Core\Routing\RequestContext;
  5. use Drupal\Component\Utility\UrlHelper;
  6. use Drupal\Core\Site\Settings;
  7. /**
  8. * A redirect response which understands domain URLs are local to the install.
  9. *
  10. * This class can be used in cases where LocalRedirectResponse needs to be
  11. * domain sensitive. The main implementation is in
  12. * DomainSourceRedirectResponseSubscriber.
  13. *
  14. * This class combines LocalAwareRedirectResponseTrait and UrlHelper methods
  15. * that cannot be overridden safely otherwise.
  16. */
  17. class DomainRedirectResponse extends CacheableSecuredRedirectResponse {
  18. /**
  19. * The request context.
  20. *
  21. * @var \Drupal\Core\Routing\RequestContext
  22. */
  23. protected $requestContext;
  24. /**
  25. * The trusted host patterns.
  26. *
  27. * @var array
  28. */
  29. protected static $trustedHostPatterns;
  30. /**
  31. * The trusted hosts matched by the settings.
  32. *
  33. * @var array
  34. */
  35. protected static $trustedHosts;
  36. /**
  37. * {@inheritdoc}
  38. */
  39. protected function isLocal($url) {
  40. $base_url = $this->getRequestContext()->getCompleteBaseUrl();
  41. return !UrlHelper::isExternal($url) || UrlHelper::externalIsLocal($url, $base_url) || $this->externalIsRegistered($url, $base_url);
  42. }
  43. /**
  44. * {@inheritdoc}
  45. */
  46. protected function isSafe($url) {
  47. return $this->isLocal($url);
  48. }
  49. /**
  50. * Returns the request context.
  51. *
  52. * @return \Drupal\Core\Routing\RequestContext
  53. * The request context.
  54. */
  55. protected function getRequestContext() {
  56. if (!isset($this->requestContext)) {
  57. $this->requestContext = \Drupal::service('router.request_context');
  58. }
  59. return $this->requestContext;
  60. }
  61. /**
  62. * Sets the request context.
  63. *
  64. * @param \Drupal\Core\Routing\RequestContext $request_context
  65. * The request context.
  66. *
  67. * @return $this
  68. */
  69. public function setRequestContext(RequestContext $request_context) {
  70. $this->requestContext = $request_context;
  71. return $this;
  72. }
  73. /**
  74. * Determines if an external URL points to this domain-aware installation.
  75. *
  76. * This method replaces the logic in
  77. * Drupal\Component\Utility\UrlHelper::externalIsLocal(). Since that class is
  78. * not directly extendable, we have to replace it.
  79. *
  80. * @param string $url
  81. * A string containing an external URL, such as "http://example.com/foo".
  82. * @param string $base_url
  83. * The base URL string to check against, such as "http://example.com/".
  84. *
  85. * @return bool
  86. * TRUE if the URL has the same domain and base path.
  87. *
  88. * @throws \InvalidArgumentException
  89. * Exception thrown when $url is not fully qualified.
  90. */
  91. public static function externalIsRegistered($url, $base_url) {
  92. $url_parts = parse_url($url);
  93. $base_parts = parse_url($base_url);
  94. if (empty($url_parts['host'])) {
  95. throw new \InvalidArgumentException('A path was passed when a fully qualified domain was expected.');
  96. }
  97. // Check that the host name is registered with trusted hosts.
  98. $trusted = self::checkTrustedHost($url_parts['host']);
  99. if (!$trusted) {
  100. return FALSE;
  101. }
  102. // Check that the requested $url is registered.
  103. $negotiator = \Drupal::service('domain.negotiator');
  104. $registered_domain = $negotiator->isRegisteredDomain($url_parts['host']);
  105. if (!isset($url_parts['path']) || !isset($base_parts['path'])) {
  106. return $registered_domain;
  107. }
  108. else {
  109. // When comparing base paths, we need a trailing slash to make sure a
  110. // partial URL match isn't occurring. Since base_path() always returns
  111. // with a trailing slash, we don't need to add the trailing slash here.
  112. return ($registered_domain && stripos($url_parts['path'], $base_parts['path']) === 0);
  113. }
  114. }
  115. /**
  116. * Checks that a host is registered with trusted_host_patterns.
  117. *
  118. * This method is cribbed from Symfony's Request::getHost() method.
  119. *
  120. * @param string $host
  121. * The hostname to check.
  122. *
  123. * @return bool
  124. * TRUE if the hostname matches the trusted_host_patterns. FALSE otherwise.
  125. * It is the caller's responsibility to deal with this result securely.
  126. */
  127. public static function checkTrustedHost($host) {
  128. // See Request::setTrustedHosts();
  129. if (!isset(self::$trustedHostPatterns)) {
  130. self::$trustedHostPatterns = array_map(function ($hostPattern) {
  131. return sprintf('#%s#i', $hostPattern);
  132. }, Settings::get('trusted_host_patterns', []));
  133. // Reset the trusted host match array.
  134. self::$trustedHosts = [];
  135. }
  136. // Trim and remove port number from host. Host is lowercase as per RFC
  137. // 952/2181.
  138. $host = mb_strtolower(preg_replace('/:\d+$/', '', trim($host)));
  139. // In the original Symfony code, hostname validation runs here. We have
  140. // removed that portion because Domains are already validated on creation.
  141. if (count(self::$trustedHostPatterns) > 0) {
  142. // To avoid host header injection attacks, you should provide a list of
  143. // trusted host patterns.
  144. if (in_array($host, self::$trustedHosts)) {
  145. return TRUE;
  146. }
  147. foreach (self::$trustedHostPatterns as $pattern) {
  148. if (preg_match($pattern, $host)) {
  149. self::$trustedHosts[] = $host;
  150. return TRUE;
  151. }
  152. }
  153. return FALSE;
  154. }
  155. // In cases where trusted_host_patterns are not set, allow all. This is
  156. // flagged as a security issue by Drupal core in the Reports UI.
  157. return TRUE;
  158. }
  159. }