RequestSanitizer.php 5.73 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Security;

5 6
use Drupal\Component\Utility\UrlHelper;
use Symfony\Component\HttpFoundation\ParameterBag;
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
use Symfony\Component\HttpFoundation\Request;

/**
 * Sanitizes user input.
 */
class RequestSanitizer {

  /**
   * Request attribute to mark the request as sanitized.
   */
  const SANITIZED = '_drupal_request_sanitized';

  /**
   * The name of the setting that configures the whitelist.
   */
  const SANITIZE_WHITELIST = 'sanitize_input_whitelist';

  /**
   * The name of the setting that determines if sanitized keys are logged.
   */
  const SANITIZE_LOG = 'sanitize_input_logging';

  /**
   * Strips dangerous keys from user input.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The incoming request to sanitize.
   * @param string[] $whitelist
   *   An array of keys to whitelist as safe. See default.settings.php.
   * @param bool $log_sanitized_keys
37
   *   (optional) Set to TRUE to log keys that are sanitized.
38 39 40 41 42 43
   *
   * @return \Symfony\Component\HttpFoundation\Request
   *   The sanitized request.
   */
  public static function sanitize(Request $request, $whitelist, $log_sanitized_keys = FALSE) {
    if (!$request->attributes->get(self::SANITIZED, FALSE)) {
44 45 46 47 48 49 50 51 52 53
      $update_globals = FALSE;
      $bags = [
        'query' => 'Potentially unsafe keys removed from query string parameters (GET): %s',
        'request' => 'Potentially unsafe keys removed from request body parameters (POST): %s',
        'cookies' => 'Potentially unsafe keys removed from cookie parameters: %s',
      ];
      foreach ($bags as $bag => $message) {
        if (static::processParameterBag($request->$bag, $whitelist, $log_sanitized_keys, $bag, $message)) {
          $update_globals = TRUE;
        }
54
      }
55 56
      if ($update_globals) {
        $request->overrideGlobals();
57
      }
58 59 60 61
      $request->attributes->set(self::SANITIZED, TRUE);
    }
    return $request;
  }
62

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
  /**
   * Processes a request parameter bag.
   *
   * @param \Symfony\Component\HttpFoundation\ParameterBag $bag
   *   The parameter bag to process.
   * @param string[] $whitelist
   *   An array of keys to whitelist as safe.
   * @param bool $log_sanitized_keys
   *   Set to TRUE to log keys that are sanitized.
   * @param string $bag_name
   *   The request parameter bag name. Either 'query', 'request' or 'cookies'.
   * @param string $message
   *   The message to log if the parameter bag contains keys that are removed.
   *   If the message contains %s that is replaced by a list of removed keys.
   *
   * @return bool
   *   TRUE if the parameter bag has been sanitized, FALSE if not.
   */
  protected static function processParameterBag(ParameterBag $bag, $whitelist, $log_sanitized_keys, $bag_name, $message) {
    $sanitized = FALSE;
    $sanitized_keys = [];
    $bag->replace(static::stripDangerousValues($bag->all(), $whitelist, $sanitized_keys));
    if (!empty($sanitized_keys)) {
      $sanitized = TRUE;
      if ($log_sanitized_keys) {
        trigger_error(sprintf($message, implode(', ', $sanitized_keys)));
89
      }
90
    }
91

92
    if ($bag->has('destination')) {
93 94
      $destination = $bag->get('destination');
      $destination_dangerous_keys = static::checkDestination($destination, $whitelist);
95 96 97 98 99 100 101 102 103
      if (!empty($destination_dangerous_keys)) {
        // The destination is removed rather than sanitized because the URL
        // generator service is not available and this method is called very
        // early in the bootstrap.
        $bag->remove('destination');
        $sanitized = TRUE;
        if ($log_sanitized_keys) {
          trigger_error(sprintf('Potentially unsafe destination removed from %s parameter bag because it contained the following keys: %s', $bag_name, implode(', ', $destination_dangerous_keys)));
        }
104
      }
105 106 107 108 109 110 111 112 113 114
      // Sanitize the destination parameter (which is often used for redirects)
      // to prevent open redirect attacks leading to other domains.
      if (UrlHelper::isExternal($destination)) {
        // The destination is removed because it is an external URL.
        $bag->remove('destination');
        $sanitized = TRUE;
        if ($log_sanitized_keys) {
          trigger_error(sprintf('Potentially unsafe destination removed from %s parameter bag because it points to an external URL.', $bag_name));
        }
      }
115
    }
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    return $sanitized;
  }

  /**
   * Checks a destination string to see if it is dangerous.
   *
   * @param string $destination
   *   The destination string to check.
   * @param array $whitelist
   *   An array of keys to whitelist as safe.
   *
   * @return array
   *   The dangerous keys found in the destination parameter.
   */
  protected static function checkDestination($destination, array $whitelist) {
    $dangerous_keys = [];
    $parts = UrlHelper::parse($destination);
    // If there is a query string, check its query parameters.
    if (!empty($parts['query'])) {
      static::stripDangerousValues($parts['query'], $whitelist, $dangerous_keys);
    }
    return $dangerous_keys;
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 168
  }

  /**
   * Strips dangerous keys from $input.
   *
   * @param mixed $input
   *   The input to sanitize.
   * @param string[] $whitelist
   *   An array of keys to whitelist as safe.
   * @param string[] $sanitized_keys
   *   An array of keys that have been removed.
   *
   * @return mixed
   *   The sanitized input.
   */
  protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) {
    if (is_array($input)) {
      foreach ($input as $key => $value) {
        if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) {
          unset($input[$key]);
          $sanitized_keys[] = $key;
        }
        else {
          $input[$key] = static::stripDangerousValues($input[$key], $whitelist, $sanitized_keys);
        }
      }
    }
    return $input;
  }

}