Verified Commit e15ccfbf authored by Juraj Nemec's avatar Juraj Nemec
Browse files

Issue #3293649 by mcdruid: drupal_http_request() fails to strip Cookie or...

Issue #3293649 by mcdruid: drupal_http_request() fails to strip Cookie or Authorization headers on HTTP downgrade
parent d07ae5de
Loading
Loading
Loading
Loading
+38 −0
Original line number Diff line number Diff line
@@ -1104,6 +1104,14 @@ function drupal_http_request($url, array $options = array()) {
        // Redirect to the new location.
        $options['max_redirects']--;

        // Check if we need to remove any potentially sensitive headers before
        // following the redirect.
        // @see https://www.rfc-editor.org/rfc/rfc9110.html#name-redirection-3xx
        if (_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location)) {
          unset($options['headers']['Cookie']);
          unset($options['headers']['Authorization']);
        }

        // We need to unset the 'Host' header
        // as we are redirecting to a new location.
        unset($options['headers']['Host']);
@@ -1122,6 +1130,36 @@ function drupal_http_request($url, array $options = array()) {
  return $result;
}

/**
 * Determine whether to strip sensitive headers from a request when redirected.
 *
 * @param string $url
 *   The url from the original outbound http request.
 *
 * @param string $location
 *   The location to which the request has been redirected.
 *
 * @return boolean
 *   Whether sensitive headers should be stripped from the request before
 *   following the redirect.
 */
function _drupal_should_strip_sensitive_headers_on_http_redirect($url, $location) {
  $url_parsed = parse_url($url);
  $location_parsed = parse_url($location);
  if (!isset($location_parsed['host'])) {
    return FALSE;
  }
  $strip_on_host_change = variable_get('drupal_http_request_strip_sensitive_headers_on_host_change', TRUE);
  $strip_on_https_downgrade = variable_get('drupal_http_request_strip_sensitive_headers_on_https_downgrade', TRUE);
  if ($strip_on_host_change && strcasecmp($url_parsed['host'], $location_parsed['host']) !== 0) {
    return TRUE;
  }
  if ($strip_on_https_downgrade && $url_parsed['scheme'] !== $location_parsed['scheme'] && 'https' !== $location_parsed['scheme']) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Splits an HTTP response status line into components.
 *
+42 −0
Original line number Diff line number Diff line
@@ -1197,6 +1197,10 @@ class DrupalHTTPRequestTestCase extends DrupalWebTestCase {
    $redirect_301 = drupal_http_request(url('system-test/redirect/301', array('absolute' => TRUE)), array('max_redirects' => 0));
    $this->assertFalse(isset($redirect_301->redirect_code), 'drupal_http_request does not follow 301 redirect if max_redirects = 0.');

    $redirect_invalid = drupal_http_request(url('system-test/redirect-protocol-relative', array('absolute' => TRUE)), array('max_redirects' => 1));
    $this->assertEqual($redirect_invalid->code, -1002, format_string('301 redirect to protocol-relative URL returned with error code !error.', array('!error' => $redirect_invalid->error)));
    $this->assertEqual($redirect_invalid->error, 'missing schema', format_string('301 redirect to protocol-relative URL returned with error message "!error".', array('!error' => $redirect_invalid->error)));

    $redirect_invalid = drupal_http_request(url('system-test/redirect-noscheme', array('absolute' => TRUE)), array('max_redirects' => 1));
    $this->assertEqual($redirect_invalid->code, -1002, format_string('301 redirect to invalid URL returned with error code !error.', array('!error' => $redirect_invalid->error)));
    $this->assertEqual($redirect_invalid->error, 'missing schema', format_string('301 redirect to invalid URL returned with error message "!error".', array('!error' => $redirect_invalid->error)));
@@ -1246,6 +1250,44 @@ class DrupalHTTPRequestTestCase extends DrupalWebTestCase {
  }
}

/**
 * Unit tests for processing of http redirects.
 */
class DrupalHTTPRedirectTest extends DrupalUnitTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Drupal HTTP redirect processing',
      'description' => 'Perform unit tests on processing of http redirects.',
      'group' => 'System',
    );
  }

  public function testHttpsDowngrade() {
    $url = 'https://example.com/foo';
    $location = 'http://example.com/bar';
    $this->assertTrue(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are stripped on HTTPS downgrade.');
  }

  public function testNoHttpsDowngrade() {
    $url = 'https://example.com/foo';
    $location = 'https://example.com/bar';
    $this->assertFalse(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are not stripped without HTTPS downgrade.');
  }

  public function testHostChange() {
    $url = 'https://example.com/foo';
    $location = 'https://www.example.com/bar';
    $this->assertTrue(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are stripped on change of host.');
  }

  public function testNoHostChange() {
    $url = 'http://example.com/foo';
    $location = 'http://example.com/bar';
    $this->assertFalse(_drupal_should_strip_sensitive_headers_on_http_redirect($url, $location), 'Sensitive headers are not stripped without change of host.');
  }
}

/**
 * Tests parsing of the HTTP response status line.
 */
+10 −0
Original line number Diff line number Diff line
@@ -45,6 +45,11 @@ function system_test_menu() {
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  $items['system-test/redirect-protocol-relative'] = array(
    'page callback' => 'system_test_redirect_protocol_relative',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  $items['system-test/redirect-noparse'] = array(
    'page callback' => 'system_test_redirect_noparse',
    'access arguments' => array('access content'),
@@ -226,6 +231,11 @@ function system_test_redirect_noscheme() {
  exit;
}

function system_test_redirect_protocol_relative() {
  header("Location: //example.com/path", TRUE, 301);
  exit;
}

function system_test_redirect_noparse() {
  header("Location: http:///path", TRUE, 301);
  exit;
+16 −0
Original line number Diff line number Diff line
@@ -805,3 +805,19 @@
 * access to all files within that scheme.
 */
# $conf['file_additional_public_schemes'] = array('example');

/**
 * Sensitive request headers in drupal_http_request() when following a redirect.
 *
 * By default drupal_http_request() will strip sensitive request headers when
 * following a redirect if the redirect location has a different http host to
 * the original request, or if the scheme downgrades from https to http.
 *
 * These variables allow opting out of this behaviour. Careful consideration of
 * the security implications of opting out is recommended.
 *
 * @see _drupal_should_strip_sensitive_headers_on_http_redirect()
 * @see drupal_http_request()
 */
# $conf['drupal_http_request_strip_sensitive_headers_on_host_change'] = TRUE;
# $conf['drupal_http_request_strip_sensitive_headers_on_https_downgrade'] = TRUE;