[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Unblock request for drupal7 7.32-1+deb8u3



Hi,

I have uploaded to unstable Drupal7 version 7.32-1+deb8u3, which fixes
the following security issue:

   https://www.drupal.org/SA-CORE-2015-001

I have backported 7.35 changes and applied them over 7.32, which is
currently in Jessie; I'm attaching the diff in question.

Please unblock drupal7 and allow this fix to migrate into Jessie!

Thanks,
diff --git a/debian/changelog b/debian/changelog
index 37c56f5..5e4f3b2 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,16 @@
+drupal7 (7.32-1+deb8u3) unstable; urgency=medium
+
+  * Added missing DEP3 headers to SA-CORE-2015-001 patch
+
+ -- Gunnar Wolf <gwolf@debian.org>  Thu, 19 Mar 2015 09:54:35 -0600
+
+drupal7 (7.32-1+deb8u2) unstable; urgency=high
+
+  * Backported from 7.35: SA-CORE-2015-001 (Access bypass on password
+    reset URLs; Open redirect)
+
+ -- Gunnar Wolf <gwolf@debian.org>  Wed, 18 Mar 2015 14:20:17 -0600
+
 drupal7 (7.32-1+deb8u1) unstable; urgency=high
 
   * Updated the VCS URL in debian/control as git.debian.org is deprecated
diff --git a/debian/patches/SA-CORE-2015-001 b/debian/patches/SA-CORE-2015-001
new file mode 100644
index 0000000..fd3de3d
--- /dev/null
+++ b/debian/patches/SA-CORE-2015-001
@@ -0,0 +1,606 @@
+Origin: vendor
+Forwarded: not-needed
+From: Gunnar Wolf <gwolf@debian.org>
+Last-Update: 2015-03-19
+Description: Fixes SA-CORE-2015-001 (Access bypass on password resetURLs; Open redirect)
+ Backporting the diff between 7.34 and 7.35, applying it to the
+ currently frozen version (7.32). For further details, the advisory is
+ in:
+ .
+ http://drupal.org/SA-CORE-2015-001
+ A CVE advisory ID has not yet been issued.
+
+Index: drupal7/includes/bootstrap.inc
+===================================================================
+--- drupal7.orig/includes/bootstrap.inc
++++ drupal7/includes/bootstrap.inc
+@@ -2487,6 +2487,26 @@ function _drupal_bootstrap_variables() {
+   // Load bootstrap modules.
+   require_once DRUPAL_ROOT . '/includes/module.inc';
+   module_load_all(TRUE);
++
++  // 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.
++  if (isset($_GET['destination']) || isset($_REQUEST['destination'])) {
++    require_once DRUPAL_ROOT . '/includes/common.inc';
++    // If the destination is an external URL, remove it.
++    if (isset($_GET['destination']) && url_is_external($_GET['destination'])) {
++      unset($_GET['destination']);
++      unset($_REQUEST['destination']);
++    }
++    // If there's still something in $_REQUEST['destination'] that didn't come
++    // from $_GET, check it too.
++    if (isset($_REQUEST['destination']) && (!isset($_GET['destination']) || $_REQUEST['destination'] != $_GET['destination']) && url_is_external($_REQUEST['destination'])) {
++      unset($_REQUEST['destination']);
++    }
++  }
+ }
+ 
+ /**
+Index: drupal7/includes/common.inc
+===================================================================
+--- drupal7.orig/includes/common.inc
++++ drupal7/includes/common.inc
+@@ -2182,14 +2182,20 @@ function url($path = NULL, array $option
+     'prefix' => ''
+   );
+ 
++  // A duplicate of the code from url_is_external() to avoid needing another
++  // function call, since performance inside url() is critical.
+   if (!isset($options['external'])) {
+-    // Return an external link if $path contains an allowed absolute URL. Only
+-    // call the slow drupal_strip_dangerous_protocols() if $path contains a ':'
+-    // before any / ? or #. Note: we could use url_is_external($path) here, but
+-    // that would require another function call, and performance inside url() is
+-    // critical.
++    // Return an external link if $path contains an allowed absolute URL. Avoid
++    // calling drupal_strip_dangerous_protocols() if there is any slash (/),
++    // hash (#) or question_mark (?) before the colon (:) occurrence - if any -
++    // as this would clearly mean it is not a URL. If the path starts with 2
++    // slashes then it is always considered an external URL without an explicit
++    // protocol part.
+     $colonpos = strpos($path, ':');
+-    $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path);
++    $options['external'] = (strpos($path, '//') === 0)
++      || ($colonpos !== FALSE
++        && !preg_match('![/?#]!', substr($path, 0, $colonpos))
++        && drupal_strip_dangerous_protocols($path) == $path);
+   }
+ 
+   // Preserve the original path before altering or aliasing.
+@@ -2227,6 +2233,11 @@ function url($path = NULL, array $option
+     return $path . $options['fragment'];
+   }
+ 
++  // Strip leading slashes from internal paths to prevent them becoming external
++  // URLs without protocol. /example.com should not be turned into
++  // //example.com.
++  $path = ltrim($path, '/');
++
+   global $base_url, $base_secure_url, $base_insecure_url;
+ 
+   // The base_url might be rewritten from the language rewrite in domain mode.
+@@ -2304,10 +2315,15 @@ function url($path = NULL, array $option
+  */
+ function url_is_external($path) {
+   $colonpos = strpos($path, ':');
+-  // Avoid calling drupal_strip_dangerous_protocols() if there is any
+-  // slash (/), hash (#) or question_mark (?) before the colon (:)
+-  // occurrence - if any - as this would clearly mean it is not a URL.
+-  return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path;
++  // Avoid calling drupal_strip_dangerous_protocols() if there is any slash (/),
++  // hash (#) or question_mark (?) before the colon (:) occurrence - if any - as
++  // this would clearly mean it is not a URL. If the path starts with 2 slashes
++  // then it is always considered an external URL without an explicit protocol
++  // part.
++  return (strpos($path, '//') === 0)
++    || ($colonpos !== FALSE
++      && !preg_match('![/?#]!', substr($path, 0, $colonpos))
++      && drupal_strip_dangerous_protocols($path) == $path);
+ }
+ 
+ /**
+@@ -2604,7 +2620,10 @@ function drupal_deliver_html_page($page_
+ 
+         // Keep old path for reference, and to allow forms to redirect to it.
+         if (!isset($_GET['destination'])) {
+-          $_GET['destination'] = $_GET['q'];
++          // Make sure that the current path is not interpreted as external URL.
++          if (!url_is_external($_GET['q'])) {
++            $_GET['destination'] = $_GET['q'];
++          }
+         }
+ 
+         $path = drupal_get_normal_path(variable_get('site_404', ''));
+@@ -2633,7 +2652,10 @@ function drupal_deliver_html_page($page_
+ 
+         // Keep old path for reference, and to allow forms to redirect to it.
+         if (!isset($_GET['destination'])) {
+-          $_GET['destination'] = $_GET['q'];
++          // Make sure that the current path is not interpreted as external URL.
++          if (!url_is_external($_GET['q'])) {
++            $_GET['destination'] = $_GET['q'];
++          }
+         }
+ 
+         $path = drupal_get_normal_path(variable_get('site_403', ''));
+Index: drupal7/modules/simpletest/tests/bootstrap.test
+===================================================================
+--- drupal7.orig/modules/simpletest/tests/bootstrap.test
++++ drupal7/modules/simpletest/tests/bootstrap.test
+@@ -546,3 +546,85 @@ class BootstrapOverrideServerVariablesTe
+     }
+   }
+ }
++
++/**
++ * Tests for $_GET['destination'] and $_REQUEST['destination'] validation.
++ */
++class BootstrapDestinationTestCase extends DrupalWebTestCase {
++
++  public static function getInfo() {
++    return array(
++      'name' => 'URL destination validation',
++      'description' => 'Test that $_GET[\'destination\'] and $_REQUEST[\'destination\'] cannot contain external URLs.',
++      'group' => 'Bootstrap',
++    );
++  }
++
++  function setUp() {
++    parent::setUp('system_test');
++  }
++
++  /**
++   * Tests that $_GET/$_REQUEST['destination'] only contain internal URLs.
++   *
++   * @see _drupal_bootstrap_variables()
++   * @see system_test_get_destination()
++   * @see system_test_request_destination()
++   */
++  public function testDestination() {
++    $test_cases = array(
++      array(
++        'input' => 'node',
++        'output' => 'node',
++        'message' => "Standard internal example node path is present in the 'destination' parameter.",
++      ),
++      array(
++        'input' => '/example.com',
++        'output' => '/example.com',
++        'message' => 'Internal path with one leading slash is allowed.',
++      ),
++      array(
++        'input' => '//example.com/test',
++        'output' => '',
++        'message' => 'External URL without scheme is not allowed.',
++      ),
++      array(
++        'input' => 'example:test',
++        'output' => 'example:test',
++        'message' => 'Internal URL using a colon is allowed.',
++      ),
++      array(
++        'input' => 'http://example.com',
++        'output' => '',
++        'message' => 'External URL is not allowed.',
++      ),
++      array(
++        'input' => 'javascript:alert(0)',
++        'output' => 'javascript:alert(0)',
++        'message' => 'Javascript URL is allowed because it is treated as an internal URL.',
++      ),
++    );
++    foreach ($test_cases as $test_case) {
++      // Test $_GET['destination'].
++      $this->drupalGet('system-test/get-destination', array('query' => array('destination' => $test_case['input'])));
++      $this->assertIdentical($test_case['output'], $this->drupalGetContent(), $test_case['message']);
++      // Test $_REQUEST['destination']. There's no form to submit to, so
++      // drupalPost() won't work here; this just tests a direct $_POST request
++      // instead.
++      $curl_parameters = array(
++        CURLOPT_URL => $this->getAbsoluteUrl('system-test/request-destination'),
++        CURLOPT_POST => TRUE,
++        CURLOPT_POSTFIELDS => 'destination=' . urlencode($test_case['input']),
++        CURLOPT_HTTPHEADER => array(),
++      );
++      $post_output = $this->curlExec($curl_parameters);
++      $this->assertIdentical($test_case['output'], $post_output, $test_case['message']);
++    }
++
++    // Make sure that 404 pages do not populate $_GET['destination'] with
++    // external URLs.
++    variable_set('site_404', 'system-test/get-destination');
++    $this->drupalGet('http://example.com', array('external' => FALSE));
++    $this->assertIdentical('', $this->drupalGetContent(), 'External URL is not allowed on 404 pages.');
++  }
++}
+Index: drupal7/modules/simpletest/tests/common.test
+===================================================================
+--- drupal7.orig/modules/simpletest/tests/common.test
++++ drupal7/modules/simpletest/tests/common.test
+@@ -209,7 +209,16 @@ class CommonURLUnitTest extends DrupalWe
+     // Test that drupal can recognize an absolute URL. Used to prevent attack vectors.
+     $this->assertTrue(url_is_external($url), 'Correctly identified an external URL.');
+ 
++    // External URL without an explicit protocol.
++    $url = '//drupal.org/foo/bar?foo=bar&bar=baz&baz#foo';
++    $this->assertTrue(url_is_external($url), 'Correctly identified an external URL without a protocol part.');
++
++    // Internal URL starting with a slash.
++    $url = '/drupal.org';
++    $this->assertFalse(url_is_external($url), 'Correctly identified an internal URL with a leading slash.');
++
+     // Test the parsing of absolute URLs.
++    $url = 'http://drupal.org/foo/bar?foo=bar&bar=baz&baz#foo';
+     $result = array(
+       'path' => 'http://drupal.org/foo/bar',
+       'query' => array('foo' => 'bar', 'bar' => 'baz', 'baz' => ''),
+@@ -349,6 +358,17 @@ class CommonURLUnitTest extends DrupalWe
+     $query = array($this->randomName(5) => $this->randomName(5));
+     $result = url($url, array('query' => $query));
+     $this->assertEqual($url . '&' . http_build_query($query, '', '&'), $result, 'External URL query string can be extended with a custom query string in $options.');
++
++    // Verify that an internal URL does not result in an external URL without
++    // protocol part.
++    $url = '/drupal.org';
++    $result = url($url);
++    $this->assertTrue(strpos($result, '//') === FALSE, 'Internal URL does not turn into an external URL.');
++
++    // Verify that an external URL without protocol part is recognized as such.
++    $url = '//drupal.org';
++    $result = url($url);
++    $this->assertEqual($url, $result, 'External URL without protocol is not altered.');
+   }
+ }
+ 
+Index: drupal7/modules/simpletest/tests/system_test.module
+===================================================================
+--- drupal7.orig/modules/simpletest/tests/system_test.module
++++ drupal7/modules/simpletest/tests/system_test.module
+@@ -106,6 +106,20 @@ function system_test_menu() {
+     'type' => MENU_CALLBACK,
+   );
+ 
++  $items['system-test/get-destination'] = array(
++    'title' => 'Test $_GET[\'destination\']',
++    'page callback' => 'system_test_get_destination',
++    'access callback' => TRUE,
++    'type' => MENU_CALLBACK,
++  );
++
++  $items['system-test/request-destination'] = array(
++    'title' => 'Test $_REQUEST[\'destination\']',
++    'page callback' => 'system_test_request_destination',
++    'access callback' => TRUE,
++    'type' => MENU_CALLBACK,
++  );
++
+   return $items;
+ }
+ 
+@@ -420,3 +434,27 @@ function system_test_authorize_init_page
+   system_authorized_init('system_test_authorize_run', drupal_get_path('module', 'system_test') . '/system_test.module', array(), $page_title);
+   drupal_goto($authorize_url);
+ }
++
++/**
++ * Page callback to print out $_GET['destination'] for testing.
++ */
++function system_test_get_destination() {
++  if (isset($_GET['destination'])) {
++    print $_GET['destination'];
++  }
++  // No need to render the whole page, we are just interested in this bit of
++  // information.
++  exit;
++}
++
++/**
++ * Page callback to print out $_REQUEST['destination'] for testing.
++ */
++function system_test_request_destination() {
++  if (isset($_REQUEST['destination'])) {
++    print $_REQUEST['destination'];
++  }
++  // No need to render the whole page, we are just interested in this bit of
++  // information.
++  exit;
++}
+Index: drupal7/modules/statistics/statistics.test
+===================================================================
+--- drupal7.orig/modules/statistics/statistics.test
++++ drupal7/modules/statistics/statistics.test
+@@ -414,7 +414,7 @@ class StatisticsAdminTestCase extends Dr
+     $timestamp = time();
+     $this->drupalPost(NULL, NULL, t('Cancel account'));
+     // Confirm account cancellation request.
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.');
+ 
+     $this->drupalGet('admin/reports/visitors');
+Index: drupal7/modules/user/user.module
+===================================================================
+--- drupal7.orig/modules/user/user.module
++++ drupal7/modules/user/user.module
+@@ -2335,7 +2335,7 @@ function user_external_login_register($n
+  */
+ function user_pass_reset_url($account) {
+   $timestamp = REQUEST_TIME;
+-  return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE));
++  return url("user/reset/$account->uid/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
+ }
+ 
+ /**
+@@ -2357,7 +2357,7 @@ function user_pass_reset_url($account) {
+  */
+ function user_cancel_url($account) {
+   $timestamp = REQUEST_TIME;
+-  return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login), array('absolute' => TRUE));
++  return url("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid), array('absolute' => TRUE));
+ }
+ 
+ /**
+@@ -2377,12 +2377,33 @@ function user_cancel_url($account) {
+  *   A UNIX timestamp, typically REQUEST_TIME.
+  * @param int $login
+  *   The UNIX timestamp of the user's last login.
++ * @param int $uid
++ *   The user ID of the user account.
+  *
+  * @return
+  *   A string that is safe for use in URLs and SQL statements.
+  */
+-function user_pass_rehash($password, $timestamp, $login) {
+-  return drupal_hmac_base64($timestamp . $login, drupal_get_hash_salt() . $password);
++function user_pass_rehash($password, $timestamp, $login, $uid) {
++  // Backwards compatibility: Try to determine a $uid if one was not passed.
++  // (Since $uid is a required parameter to this function, a PHP warning will
++  // be generated if it's not provided, which is an indication that the calling
++  // code should be updated. But the code below will try to generate a correct
++  // hash in the meantime.)
++  if (!isset($uid)) {
++    $uids = db_query_range('SELECT uid FROM {users} WHERE pass = :password AND login = :login AND uid > 0', 0, 2, array(':password' => $password, ':login' => $login))->fetchCol();
++    // If exactly one user account matches the provided password and login
++    // timestamp, proceed with that $uid.
++    if (count($uids) == 1) {
++      $uid = reset($uids);
++    }
++    // Otherwise there is no safe hash to return, so return a random string
++    // that will never be treated as a valid token.
++    else {
++      return drupal_random_key();
++    }
++  }
++
++  return drupal_hmac_base64($timestamp . $login . $uid, drupal_get_hash_salt() . $password);
+ }
+ 
+ /**
+Index: drupal7/modules/user/user.pages.inc
+===================================================================
+--- drupal7.orig/modules/user/user.pages.inc
++++ drupal7/modules/user/user.pages.inc
+@@ -126,7 +126,7 @@ function user_pass_reset($form, &$form_s
+         drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
+         drupal_goto('user/password');
+       }
+-      elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
++      elseif ($account->uid && $timestamp >= $account->login && $timestamp <= $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
+         // First stage is a confirmation form, then login
+         if ($action == 'login') {
+           // Set the new user.
+@@ -523,7 +523,7 @@ function user_cancel_confirm($account, $
+   // Basic validation of arguments.
+   if (isset($account->data['user_cancel_method']) && !empty($timestamp) && !empty($hashed_pass)) {
+     // Validate expiration and hashed password/login.
+-    if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
++    if ($timestamp <= $current && $current - $timestamp < $timeout && $account->uid && $timestamp >= $account->login && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid)) {
+       $edit = array(
+         'user_cancel_notify' => isset($account->data['user_cancel_notify']) ? $account->data['user_cancel_notify'] : variable_get('user_mail_status_canceled_notify', FALSE),
+       );
+Index: drupal7/modules/user/user.test
+===================================================================
+--- drupal7.orig/modules/user/user.test
++++ drupal7/modules/user/user.test
+@@ -498,7 +498,7 @@ class UserPasswordResetTestCase extends
+     // To attempt an expired password reset, create a password reset link as if
+     // its request time was 60 seconds older than the allowed limit of timeout.
+     $bogus_timestamp = REQUEST_TIME - variable_get('user_password_reset_timeout', 86400) - 60;
+-    $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login));
++    $this->drupalGet("user/reset/$account->uid/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+     $this->assertText(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'), 'Expired password reset request rejected.');
+   }
+ 
+@@ -519,6 +519,74 @@ class UserPasswordResetTestCase extends
+     $this->assertFieldByName('name', $edit['name'], 'User name found.');
+   }
+ 
++  /**
++   * Make sure that users cannot forge password reset URLs of other users.
++   */
++  function testResetImpersonation() {
++    // Make sure user 1 has a valid password, so it does not interfere with the
++    // test user accounts that are created below.
++    $account = user_load(1);
++    user_save($account, array('pass' => user_password()));
++
++    // Create two identical user accounts except for the user name. They must
++    // have the same empty password, so we can't use $this->drupalCreateUser().
++    $edit = array();
++    $edit['name'] = $this->randomName();
++    $edit['mail'] = $edit['name'] . '@example.com';
++    $edit['status'] = 1;
++
++    $user1 = user_save(drupal_anonymous_user(), $edit);
++
++    $edit['name'] = $this->randomName();
++    $user2 = user_save(drupal_anonymous_user(), $edit);
++
++    // The password reset URL must not be valid for the second user when only
++    // the user ID is changed in the URL.
++    $reset_url = user_pass_reset_url($user1);
++    $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url);
++    $this->drupalGet($attack_reset_url);
++    $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.');
++    $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
++    $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
++
++    // When legacy code calls user_pass_rehash() without providing the $uid
++    // parameter, neither password reset URL should be valid since it is
++    // impossible for the system to determine which user account the token was
++    // intended for.
++    $timestamp = REQUEST_TIME;
++    // Pass an explicit NULL for the $uid parameter of user_pass_rehash()
++    // rather than not passing it at all, to avoid triggering PHP warnings in
++    // the test.
++    $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
++    $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE));
++    $this->drupalGet($reset_url);
++    $this->assertNoText($user1->name, 'The invalid password reset page does not show the user name.');
++    $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
++    $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
++    $attack_reset_url = str_replace("user/reset/$user1->uid", "user/reset/$user2->uid", $reset_url);
++    $this->drupalGet($attack_reset_url);
++    $this->assertNoText($user2->name, 'The invalid password reset page does not show the user name.');
++    $this->assertUrl('user/password', array(), 'The user is redirected to the password reset request page.');
++    $this->assertText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
++
++    // To verify that user_pass_rehash() never returns a valid result in the
++    // above situation (even if legacy code also called it to attempt to
++    // validate the token, rather than just to generate the URL), check that a
++    // second call with the same parameters produces a different result.
++    $new_reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
++    $this->assertNotEqual($reset_url_token, $new_reset_url_token);
++
++    // However, when the duplicate account is removed, the password reset URL
++    // should be valid.
++    user_delete($user2->uid);
++    $reset_url_token = user_pass_rehash($user1->pass, $timestamp, $user1->login, NULL);
++    $reset_url = url("user/reset/$user1->uid/$timestamp/$reset_url_token", array('absolute' => TRUE));
++    $this->drupalGet($reset_url);
++    $this->assertText($user1->name, 'The valid password reset page shows the user name.');
++    $this->assertUrl($reset_url, array(), 'The user remains on the password reset login page.');
++    $this->assertNoText('You have tried to use a one-time login link that has either been used or is no longer valid. Please request a new one using the form below.');
++  }
++
+ }
+ 
+ /**
+@@ -558,7 +626,7 @@ class UserCancelTestCase extends DrupalW
+ 
+     // Attempt bogus account cancellation request confirmation.
+     $timestamp = $account->login;
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $this->assertResponse(403, 'Bogus cancelling request rejected.');
+     $account = user_load($account->uid);
+     $this->assertTrue($account->status == 1, 'User account was not canceled.');
+@@ -631,14 +699,14 @@ class UserCancelTestCase extends DrupalW
+ 
+     // Attempt bogus account cancellation request confirmation.
+     $bogus_timestamp = $timestamp + 60;
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+     $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Bogus cancelling request rejected.');
+     $account = user_load($account->uid);
+     $this->assertTrue($account->status == 1, 'User account was not canceled.');
+ 
+     // Attempt expired account cancellation request confirmation.
+     $bogus_timestamp = $timestamp - 86400 - 60;
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$bogus_timestamp/" . user_pass_rehash($account->pass, $bogus_timestamp, $account->login, $account->uid));
+     $this->assertText(t('You have tried to use an account cancellation link that has expired. Please request a new one using the form below.'), 'Expired cancel account request rejected.');
+     $accounts = user_load_multiple(array($account->uid), array('status' => 1));
+     $this->assertTrue(reset($accounts), 'User account was not canceled.');
+@@ -675,7 +743,7 @@ class UserCancelTestCase extends DrupalW
+     $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+ 
+     // Confirm account cancellation request.
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $account = user_load($account->uid, TRUE);
+     $this->assertTrue($account->status == 0, 'User has been blocked.');
+ 
+@@ -713,7 +781,7 @@ class UserCancelTestCase extends DrupalW
+     $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+ 
+     // Confirm account cancellation request.
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $account = user_load($account->uid, TRUE);
+     $this->assertTrue($account->status == 0, 'User has been blocked.');
+ 
+@@ -763,7 +831,7 @@ class UserCancelTestCase extends DrupalW
+     $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+ 
+     // Confirm account cancellation request.
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.');
+ 
+     // Confirm that user's content has been attributed to anonymous user.
+@@ -827,7 +895,7 @@ class UserCancelTestCase extends DrupalW
+     $this->assertText(t('A confirmation request to cancel your account has been sent to your e-mail address.'), 'Account cancellation request mailed message displayed.');
+ 
+     // Confirm account cancellation request.
+-    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login));
++    $this->drupalGet("user/$account->uid/cancel/confirm/$timestamp/" . user_pass_rehash($account->pass, $timestamp, $account->login, $account->uid));
+     $this->assertFalse(user_load($account->uid, TRUE), 'User is not found in the database.');
+ 
+     // Confirm that user's content has been deleted.
+Index: drupal7/modules/system/system.test
+===================================================================
+--- drupal7.orig/modules/system/system.test
++++ drupal7/modules/system/system.test
+@@ -2797,3 +2797,46 @@ class SystemValidTokenTest extends Drupa
+     return TRUE;
+   }
+ }
++
++/**
++ * Tests confirm form destinations.
++ */
++class ConfirmFormTest extends DrupalWebTestCase {
++  protected $admin_user;
++
++  public static function getInfo() {
++    return array(
++      'name' => 'Confirm form',
++      'description' => 'Tests that the confirm form does not use external destinations.',
++      'group' => 'System',
++    );
++  }
++
++  function setUp() {
++    parent::setUp();
++
++    $this->admin_user = $this->drupalCreateUser(array('administer users'));
++    $this->drupalLogin($this->admin_user);
++  }
++
++  /**
++   * Tests that the confirm form does not use external destinations.
++   */
++  function testConfirmForm() {
++    $this->drupalGet('user/1/cancel');
++    $this->assertCancelLinkUrl(url('user/1'));
++    $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'node')));
++    $this->assertCancelLinkUrl(url('node'));
++    $this->drupalGet('user/1/cancel', array('query' => array('destination' => 'http://example.com')));
++    $this->assertCancelLinkUrl(url('user/1'));
++  }
++
++  /**
++   * Asserts that a cancel link is present pointing to the provided URL.
++   */
++  function assertCancelLinkUrl($url, $message = '', $group = 'Other') {
++    $links = $this->xpath('//a[normalize-space(text())=:label and @href=:url]', array(':label' => t('Cancel'), ':url' => $url));
++    $message = ($message ? $message : format_string('Cancel link with url %url found.', array('%url' => $url)));
++    return $this->assertTrue(isset($links[0]), $message, $group);
++  }
++}
diff --git a/debian/patches/series b/debian/patches/series
index 9ff113c..2fbf10a 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -7,3 +7,4 @@ db_sanitize_orderby
 ajax_throbber_align
 fix_field_has_data_return
 dont_lose_user_pictures
+SA-CORE-2015-001

Attachment: signature.asc
Description: Digital signature


Reply to: