Index: captcha.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/captcha/captcha.install,v
retrieving revision 1.11.2.1
diff -u -b -u -p -r1.11.2.1 captcha.install
--- captcha.install	1 Jun 2010 21:39:22 -0000	1.11.2.1
+++ captcha.install	19 Jun 2010 16:55:33 -0000
@@ -37,6 +37,12 @@ function captcha_schema() {
         'type' => 'serial',
         'not null' => TRUE,
       ),
+      'token' => array(
+        'description' => 'One time CAPTCHA token.',
+        'type' => 'varchar',
+        'length' => 64,
+        'not null' => FALSE,
+      ),
       'uid' => array(
         'description' => "User's {users}.uid.",
         'type' => 'int',
@@ -289,3 +295,15 @@ function captcha_update_6201() {
   $items[] = update_sql("UPDATE {captcha_points} SET module = 'captcha', type = 'Math' WHERE module = 'text_captcha' AND type = 'Text';");
   return $items;
 }
+
+
+/**
+ * Implementation of hook_update_N()
+ * Add a CAPTCHA token column to captcha_sessions table.
+ */
+function captcha_update_6202() {
+  $ret = array();
+  db_add_column(&$ret, 'captcha_sessions', 'token', 'varchar(64)');
+  return $ret;
+}
+
Index: captcha.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/captcha/captcha.module,v
retrieving revision 1.103.2.7
diff -u -b -u -p -r1.103.2.7 captcha.module
--- captcha.module	12 Jun 2010 14:35:55 -0000	1.103.2.7
+++ captcha.module	19 Jun 2010 16:55:34 -0000
@@ -172,6 +172,7 @@ function captcha_elements() {
  * Process callback for CAPTCHA form element.
  */
 function captcha_process($element, $edit, &$form_state, $complete_form) {
+
   module_load_include('inc', 'captcha');
 
   // Prevent caching of the page with CAPTCHA elements.
@@ -203,17 +204,34 @@ function captcha_process($element, $edit
     '#value' => $captcha_sid,
   );
 
+  // Additional one time CAPTCHA token: store in database and send with form.
+  $captcha_token = md5(mt_rand());
+  db_query("UPDATE {captcha_sessions} SET token='%s' WHERE csid=%d", $captcha_token, $captcha_sid);
+  $element['captcha_token'] = array(
+    '#type' => 'hidden',
+    '#value' => $captcha_token,
+  );
+
   // Get implementing module and challenge for CAPTCHA.
   list($captcha_type_module, $captcha_type_challenge) = _captcha_parse_captcha_type($element['#captcha_type']);
 
-  // Store CAPTCHA information (e.g. for usage in the validation and pre_render phases).
-  $element['#captcha_info'] = array(
-    'form_id' => $this_form_id,
+  // Store CAPTCHA information for further processing in
+  // - $form_state['captcha_info'], which survives a form rebuild (e.g. node
+  //   preview), useful in _captcha_get_posted_captcha_info().
+  // - $element['#captcha_info'], for post processing functions that do not
+  //   receive a $form_state argument (e.g. the pre_render callback).
+  $form_state['captcha_info'] = array(
+    'form_id' => $posted_form_id,
+    'captcha_sid' => $captcha_sid,
     'module' => $captcha_type_module,
     'type' => $captcha_type_challenge,
+  );
+  $element['#captcha_info'] = array(
+    'form_id' => $this_form_id,
     'captcha_sid' => $captcha_sid,
   );
 
+
   if (_captcha_required_for_user($captcha_sid, $this_form_id) || $element['#captcha_admin_mode']) {
     // Generate a CAPTCHA and its solution
     // (note that the CAPTCHA session ID is given as third argument).
@@ -409,6 +427,7 @@ function captcha_validate_case_insensiti
   return preg_replace('/\s/', '', strtolower($solution)) == preg_replace('/\s/', '', strtolower($response));
 }
 
+
 /**
  * Helper function for getting the posted CAPTCHA info (posted form_id and
  * CAPTCHA sessions ID) from a form in case it is posted.
@@ -433,7 +452,15 @@ function captcha_validate_case_insensiti
  *     if the values could not be found, e.g. for a fresh form).
  */
 function _captcha_get_posted_captcha_info($element, $form_state) {
-  // Get the post data from where we can find it.
+  if (isset($form_state['captcha_info'])) {
+    // We already determined the posted form ID and CAPTCHA session ID
+    // for this form, so we reuse this info
+    $posted_form_id = $form_state['captcha_info']['form_id'];
+    $posted_captcha_sid = $form_state['captcha_info']['captcha_sid'];
+  }
+  else {
+    // We have to determine the posted form ID and CAPTCHA session ID
+    // from the post data. We have to consider some sources for the post data.
   if (isset($element['#post']) && count($element['#post'])) {
     $post_data = $element['#post'];
   }
@@ -447,9 +474,23 @@ function _captcha_get_posted_captcha_inf
     // No posted CAPTCHA info found (probably a fresh form).
     $post_data = array();
   }
-  // Return the posted form_id and CAPTCHA session ID.
+    // Get the posted form_id and CAPTCHA session ID.
   $posted_form_id = isset($post_data['form_id']) ? $post_data['form_id'] : NULL;
-  $posted_captcha_sid = isset($post_data['captcha_sid']) ? $post_data['captcha_sid'] : NULL;
+    $posted_captcha_sid = isset($post_data['captcha_sid']) ? (int) $post_data['captcha_sid'] : NULL;
+    $posted_captcha_token = isset($post_data['captcha_token']) ? (string) $post_data['captcha_token'] : NULL;
+
+    // Check if the posted CAPTCHA token is valid for the posted CAPTCHA session.
+    if ($posted_captcha_sid != NULL) {
+      $expected_captcha_token = db_result(db_query("SELECT token FROM {captcha_sessions} WHERE csid = %d", $posted_captcha_sid));
+      if ($expected_captcha_token !== $posted_captcha_token) {
+        drupal_set_message(t('CAPTCHA session reuse attack detected.'), 'error');
+        // Invalidate the CAPTCHA session.
+        $posted_captcha_sid = NULL;
+      }
+      // Invalidate CAPTCHA token to avoid reuse.
+      db_query("UPDATE {captcha_sessions} SET token=NULL WHERE csid=%d", $posted_captcha_sid);
+    }
+  }
   return array($posted_form_id, $posted_captcha_sid);
 }
 
@@ -462,27 +503,23 @@ function _captcha_get_posted_captcha_inf
  * files).
  */
 function captcha_validate($element, &$form_state) {
-  $captcha_info = $element['#captcha_info'];
+
+  $captcha_info = $form_state['captcha_info'];
   $form_id = $captcha_info['form_id'];
 
   // Get CAPTCHA response.
   $captcha_response = $form_state['values']['captcha_response'];
 
-  // We use the posted CAPTCHA session ID instead of
-  // $form_state['values']['captcha_sid'] because the latter contains the
-  // captcha_sid associated to the 'newly' generated element,
-  // while the former contains the captcha_sid of the posted form.
-  // In most cases both will be the same because of persistence.
-  // However, they will differ when the life span of the CAPTCHA session
-  // does not equal the life span of a multipage form and then we have to
-  // pick the right one.
-  list($posted_form_id, $posted_captcha_sid) = _captcha_get_posted_captcha_info($element, $form_state);
-  $csid = $posted_captcha_sid;
+  // Get CAPTCHA session from CAPTCHA info
+  // TODO: is this correct in all cases: see comment and code in previous revisions?
+  $csid = $captcha_info['captcha_sid'];
 
   $solution = db_result(db_query('SELECT solution FROM {captcha_sessions} WHERE csid = %d', $csid));
 
   if ($solution === FALSE) {
     // Unknown challenge_id.
+    // TODO: this probably never happens anymore now that there is detection
+    // for CAPTCHA session reuse attacks in _captcha_get_posted_captcha_info().
     form_set_error('captcha', t('CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.'));
     watchdog('CAPTCHA',
       'CAPTCHA validation error: unknown CAPTCHA session ID (%csid).',
Index: captcha.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/captcha/captcha.test,v
retrieving revision 1.14.2.3
diff -u -b -u -p -r1.14.2.3 captcha.test
--- captcha.test	12 Jun 2010 14:35:55 -0000	1.14.2.3
+++ captcha.test	19 Jun 2010 16:55:34 -0000
@@ -15,29 +15,132 @@
 // TODO: test space ignoring validation of image CAPTCHA
 // TODO: test that forged CAPTCHA session id raises a "CAPTCHA validation error".
 
-class CapchaTestCase extends DrupalWebTestCase {
+// Some constants for better reuse.
+define('CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE',
+  'The answer you entered for the CAPTCHA was not correct.');
 
-  public static function getInfo() {
-    return array(
-      'name' => t('CAPTCHA functionality'),
-      'description' => t('Testing of the basic CAPTCHA functionality.'),
-      'group' => t('CAPTCHA'),
-    );
-  }
+define('CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE',
+  'CAPTCHA session reuse attack detected.');
+
+define('CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE',
+  'CAPTCHA validation error: unknown CAPTCHA session ID. Contact the site administrator if this problem persists.');
+
+/**
+ * Base class for CAPTCHA tests.
+ *
+ * Provides common setup stuff and various helper functions
+ */
+class CaptchaBaseWebTestCase extends DrupalWebTestCase {
 
   function setUp() {
     // Load two modules: the captcha module itself and the comment module for testing anonymous comments.
     parent::setUp('captcha', 'comment');
     module_load_include('inc', 'captcha');
 
-    // Create a normal user that can post comments and pages,
-    // but can not skip CAPTCHA.
+    // Create a normal user.
     $permissions = array(
       'access comments', 'post comments', 'post comments without approval',
       'access content', 'create page content', 'edit own page content',
     );
     $this->normal_user = $this->drupalCreateUser($permissions);
 
+    // Create an admin user.
+    $permissions[] = 'administer CAPTCHA settings';
+    $permissions[] = 'skip CAPTCHA';
+    $this->admin_user = $this->drupalCreateUser($permissions);
+
+    // Create a node with comments enabled.
+    $this->comment_node = $this->drupalCreateNode(array('comment' => COMMENT_NODE_READ_WRITE));
+
+  }
+
+  /**
+   * Assert that the response is accepted:
+   * no "unknown CSID" message, no "CSID reuse attack detection" message,
+   * no "wrong answer" message.
+   */
+  protected function assertCaptchaResponseAccepted() {
+    // There should be no error message about unknown CAPTCHA session ID.
+    $this->assertNoText(
+      t(CAPTCHA_UNKNOWN_CSID_ERROR_MESSAGE),
+      'CAPTCHA response should be accepted (known CSID).',
+      'CAPTCHA'
+    );
+    // There should be no error message about CSID reuse attack.
+    $this->assertNoText(
+      t(CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE),
+      'CAPTCHA response should be accepted (no CAPTCHA session reuse attack detection).',
+      'CAPTCHA'
+    );
+    // There should be no error message about wrong response.
+    $this->assertNoText(
+      t(CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
+      'CAPTCHA response should be accepted (correct response).',
+      'CAPTCHA'
+    );
+  }
+
+  /**
+   * Assert that there is a CAPTCHA on the form or not.
+   * @param bool $presence whether there should be a CAPTCHA or not.
+   */
+  protected function assertCaptchaPresence($presence) {
+    if ($presence) {
+      $this->assertText(_captcha_get_description(),
+        'There should be a CAPTCHA on the form.', 'CAPTCHA');
+    }
+    else {
+      $this->assertNoText(_captcha_get_description(),
+        'There should be no CAPTCHA on the form.', 'CAPTCHA');
+    }
+  }
+
+  /**
+   * Get the CAPTCHA session id from the current form in the browser.
+   */
+  protected function getCaptchaSidFromForm() {
+    $elements = $this->xpath('//input[@name="captcha_sid"]');
+    $captcha_sid = (int) $elements[0]['value'];
+    return $captcha_sid;
+  }
+  /**
+   * Get the CAPTCHA token from the current form in the browser.
+   */
+  protected function getCaptchaTokenFromForm() {
+    $elements = $this->xpath('//input[@name="captcha_token"]');
+    $captcha_token = (int) $elements[0]['value'];
+    return $captcha_token;
+  }
+
+  /**
+   * Get the solution of the math CAPTCHA from the current form in the browser.
+   */
+  protected function getMathCaptchaSolutionFromForm() {
+    // Get the math challenge.
+    $elements = $this->xpath('//div[@id="edit-captcha-response-wrapper"]/span[@class="field-prefix"]');
+    $challenge = (string) $elements[0];
+    // Extract terms and operator from challenge.
+    $matches = array();
+    $ret = preg_match('/\\s*(\\d+)\\s*(-|\\+)\\s*(\\d+)\\s*=\\s*/', $challenge, $matches);
+    // Solve the challenge
+    $a = (int) $matches[1];
+    $b = (int) $matches[3];
+    $solution = $matches[2] == '-' ? $a - $b : $a + $b;
+    return $solution;
+  }
+
+}
+
+
+
+class CaptchaTestCase extends CaptchaBaseWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('CAPTCHA functionality'),
+      'description' => t('Testing of the basic CAPTCHA functionality.'),
+      'group' => t('CAPTCHA'),
+    );
   }
 
   /**
@@ -55,9 +158,7 @@ class CapchaTestCase extends DrupalWebTe
 
     // Check if there is a CAPTCHA on the login form (look for the title).
     $this->drupalGet('user');
-    $captcha = captcha_captcha('generate', 'Math');
-    $this->assertText($captcha['form']['captcha_response']['#title'], 'CAPTCHA should be added to form (user_login).', 'CAPTCHA');
-
+    $this->assertCaptchaPresence(TRUE);
 
     // Try to log in, which should fail.
     $edit = array(
@@ -67,7 +168,7 @@ class CapchaTestCase extends DrupalWebTe
     );
     $this->drupalPost('user', $edit, t('Log in'));
     // Check for error message.
-    $this->assertText(t('The answer you entered for the CAPTCHA was not correct.'),
+    $this->assertText(t(CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
       'CAPTCHA should block user login form', 'CAPTCHA');
 
     // And make sure that user is not logged in: check for name and password fields on ?q=user
@@ -110,8 +211,7 @@ class CapchaTestCase extends DrupalWebTe
 
     if ($should_pass) {
       // There should be no error message.
-      $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-        $message .' Comment submission should pass.', 'CAPTCHA');
+      $this->assertCaptchaResponseAccepted();
       // Get node page and check that comment shows up.
       $this->drupalGet('node/' . $node->nid);
       $this->assertText($edit['comment'],
@@ -119,7 +219,7 @@ class CapchaTestCase extends DrupalWebTe
     }
     else {
       // Check for error message.
-      $this->assertText(t('The answer you entered for the CAPTCHA was not correct.'),
+      $this->assertText(t(CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
         $message .' Comment submission should be blocked.', 'CAPTCHA');
       // Get node page and check that comment is not present.
       $this->drupalGet('node/' . $node->nid);
@@ -179,8 +279,7 @@ class CapchaTestCase extends DrupalWebTe
     $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
 
     // Check that there is no CAPTCHA after preview.
-    $this->assertNoText(_captcha_get_description(),
-      'CAPTCHA description should not show up after comment preview with correct response.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
   }
 
   /**
@@ -208,14 +307,13 @@ class CapchaTestCase extends DrupalWebTe
     $this->drupalPost('node/add/page', $edit, t('Preview'));
 
     // Check that there is no CAPTCHA after preview.
-    $this->assertNoText(_captcha_get_description(),
-      'CAPTCHA description should not show up after node preview with correct response.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
   }
 
 }
 
 
-class CapchaAdminTestCase extends DrupalWebTestCase {
+class CaptchaAdminTestCase extends CaptchaBaseWebTestCase {
 
   public static function getInfo() {
     return array(
@@ -225,23 +323,6 @@ class CapchaAdminTestCase extends Drupal
     );
   }
 
-  function setUp() {
-    // Load two modules: the captcha module itself and the comment module for testing anonymous comments.
-    parent::setUp('captcha', 'comment');
-    module_load_include('inc', 'captcha');
-
-    // Create a normal user.
-    $permissions = array(
-      'access comments', 'post comments', 'post comments without approval',
-    );
-    $this->normal_user = $this->drupalCreateUser($permissions);
-    // Create an admin user.
-    $permissions[] = 'administer CAPTCHA settings';
-    $permissions[] = 'skip CAPTCHA';
-    $this->admin_user = $this->drupalCreateUser($permissions);
-
-  }
-
   /**
    * Test access to the admin pages.
    */
@@ -419,8 +500,7 @@ class CapchaAdminTestCase extends Drupal
     $add_comment_url = $this->getUrl();
 
     // Check if CAPTCHA is visible on form.
-    $this->assertText(t('Math question'),
-      'CAPTCHA should be on form for untrusted user.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     // Try to post a comment with wrong answer.
     $edit = array(
       'subject' => 'check thiz out!',
@@ -428,7 +508,7 @@ class CapchaAdminTestCase extends Drupal
       'captcha_response' => 'xx',
     );
     $this->drupalPost($add_comment_url, $edit, t('Preview'));
-    $this->assertText(t('The answer you entered for the CAPTCHA was not correct.'),
+    $this->assertText(t(CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
       'wrong CAPTCHA should block form submission.', 'CAPTCHA');
 
     //TODO: more testing for untrusted posts.
@@ -460,7 +540,7 @@ class CapchaAdminTestCase extends Drupal
 
 
 
-class CapchaPersistenceTestCase extends DrupalWebTestCase {
+class CaptchaPersistenceTestCase extends CaptchaBaseWebTestCase {
 
   public static function getInfo() {
     return array(
@@ -470,23 +550,6 @@ class CapchaPersistenceTestCase extends 
     );
   }
 
-  function setUp() {
-    // Load two modules: the captcha module itself and the comment module for testing anonymous comments.
-    parent::setUp('captcha', 'comment');
-    module_load_include('inc', 'captcha');
-
-    // Create a normal user.
-    $permissions = array(
-      'access comments', 'post comments', 'post comments without approval',
-    );
-    $this->normal_user = $this->drupalCreateUser($permissions);
-
-    // Create an admin user.
-    $permissions[] = 'administer CAPTCHA settings';
-    $permissions[] = 'skip CAPTCHA';
-    $this->admin_user = $this->drupalCreateUser($permissions);
-  }
-
   /**
    * Set up the persistence and CAPTCHA settings.
    * @param int $persistence the persistence value.
@@ -506,27 +569,23 @@ class CapchaPersistenceTestCase extends 
     // We also have to do this after all usage of the CAPTCHA admin form
     // (because posting the CAPTCHA admin form would set the CAPTCHA to 'none').
     captcha_set_form_id_setting('user_login', 'captcha/Test');
+    $this->drupalGet('user');
+    $this->assertCaptchaPresence(TRUE);
     captcha_set_form_id_setting('user_register', 'captcha/Test');
-    captcha_set_form_id_setting('comment_form', 'captcha/Test');
-  }
-
-  /**
-   * Helper function to get the CAPTCHA session id from the current page.
-   */
-  private function getCaptchaSidFromForm() {
-    $elements = $this->xpath('//input[@name="captcha_sid"]');
-    $captcha_sid = (int) $elements[0]['value'];
-    return $captcha_sid;
+    $this->drupalGet('user/register');
+    $this->assertCaptchaPresence(TRUE);
   }
 
   protected function assertPreservedCsid($captcha_sid_initial) {
     $captcha_sid = $this->getCaptchaSidFromForm();
-    $this->assertEqual($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be preserved (expected: $captcha_sid_initial, found: $captcha_sid).");
+    $this->assertEqual($captcha_sid_initial, $captcha_sid,
+      "CAPTCHA session ID should be preserved (expected: $captcha_sid_initial, found: $captcha_sid).");
   }
 
   protected function assertDifferentCsid($captcha_sid_initial) {
     $captcha_sid = $this->getCaptchaSidFromForm();
-    $this->assertNotEqual($captcha_sid_initial, $captcha_sid, "CAPTCHA session ID should be different.");
+    $this->assertNotEqual($captcha_sid_initial, $captcha_sid,
+      "CAPTCHA session ID should be different.");
   }
 
   function testPersistenceAlways(){
@@ -535,9 +594,7 @@ class CapchaPersistenceTestCase extends 
 
     // Go to login form and check if there is a CAPTCHA on the login form (look for the title).
     $this->drupalGet('user');
-    $captcha = captcha_captcha('generate', 'Test');
-    $captcha_text = $captcha['form']['captcha_response']['#title'];
-    $this->assertText($captcha_text, 'First view of form should have CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $captcha_sid_initial = $this->getCaptchaSidFromForm();
 
     // Try to with wrong user name and password, but correct CAPTCHA.
@@ -548,24 +605,16 @@ class CapchaPersistenceTestCase extends 
     );
     $this->drupalPost(NULL, $edit, t('Log in'));
     // Check that there was no error message for the CAPTCHA.
-    $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-      'CAPTCHA answer should be correct', 'CAPTCHA');
+    $this->assertCaptchaResponseAccepted();
 
     // Name and password were wrong, we should get an updated form with a fresh CAPTCHA.
-    $this->assertText($captcha_text, 'Updated form (after correct CAPTCHA) should have another CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $this->assertPreservedCsid($captcha_sid_initial);
 
     // Post from again.
     $this->drupalPost(NULL, $edit, t('Log in'));
     // Check that there was no error message for the CAPTCHA.
-    $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-      'CAPTCHA answer should be correct', 'CAPTCHA');
-    // Also check that there is no CAPTCHA validation error (because we are
-    // solving a new CAPTCHA in the same CAPTCHA session).
-    $this->assertNoText(t('CAPTCHA validation error'),
-      'There should be no CAPTCHA validation error', 'CAPTCHA');
-    $captcha_sid = $this->getCaptchaSidFromForm();
-
+    $this->assertCaptchaResponseAccepted();
     $this->assertPreservedCsid($captcha_sid_initial);
 
   }
@@ -576,9 +625,7 @@ class CapchaPersistenceTestCase extends 
 
     // Go to login form and check if there is a CAPTCHA on the login form.
     $this->drupalGet('user');
-    $captcha = captcha_captcha('generate', 'Test');
-    $captcha_text = $captcha['form']['captcha_response']['#title'];
-    $this->assertText($captcha_text, 'First view of new form session should have CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $captcha_sid_initial = $this->getCaptchaSidFromForm();
 
     // Try to with wrong user name and password, but correct CAPTCHA.
@@ -589,21 +636,20 @@ class CapchaPersistenceTestCase extends 
     );
     $this->drupalPost(NULL, $edit, t('Log in'));
     // Check that there was no error message for the CAPTCHA.
-    $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-      'CAPTCHA answer should be correct', 'CAPTCHA');
+    $this->assertCaptchaResponseAccepted();
     // There shouldn't be a CAPTCHA on the new form.
-    $this->assertNoText($captcha_text, 'Updated form (after correct CAPTCHA) should have no CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertPreservedCsid($captcha_sid_initial);
 
     // Start a new form instance/session
     $this->drupalGet('node');
     $this->drupalGet('user');
-    $this->assertText($captcha_text, 'A new session should have a CAPTCHA again.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $this->assertDifferentCsid($captcha_sid_initial);
 
     // Check another form
     $this->drupalGet('user/register');
-    $this->assertText($captcha_text, 'Another form should still have a CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $this->assertDifferentCsid($captcha_sid_initial);
 
   }
@@ -614,9 +660,7 @@ class CapchaPersistenceTestCase extends 
 
     // Go to login form and check if there is a CAPTCHA on the login form.
     $this->drupalGet('user');
-    $captcha = captcha_captcha('generate', 'Test');
-    $captcha_text = $captcha['form']['captcha_response']['#title'];
-    $this->assertText($captcha_text, 'First view of new form session should have CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $captcha_sid_initial = $this->getCaptchaSidFromForm();
 
     // Try to with wrong user name and password, but correct CAPTCHA.
@@ -627,21 +671,20 @@ class CapchaPersistenceTestCase extends 
     );
     $this->drupalPost(NULL, $edit, t('Log in'));
     // Check that there was no error message for the CAPTCHA.
-    $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-      'CAPTCHA answer should be correct', 'CAPTCHA');
+    $this->assertCaptchaResponseAccepted();
     // There shouldn't be a CAPTCHA on the new form.
-    $this->assertNoText($captcha_text, 'Updated form (after correct CAPTCHA) should have no CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertPreservedCsid($captcha_sid_initial);
 
     // Start a new form instance/session
     $this->drupalGet('node');
     $this->drupalGet('user');
-    $this->assertNoText($captcha_text, 'A new session should also have no CAPTCHA anymore.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertDifferentCsid($captcha_sid_initial);
 
     // Check another form
     $this->drupalGet('user/register');
-    $this->assertText($captcha_text, 'Another form should still have a CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $this->assertDifferentCsid($captcha_sid_initial);
   }
 
@@ -651,9 +694,7 @@ class CapchaPersistenceTestCase extends 
 
     // Go to login form and check if there is a CAPTCHA on the login form.
     $this->drupalGet('user');
-    $captcha = captcha_captcha('generate', 'Test');
-    $captcha_text = $captcha['form']['captcha_response']['#title'];
-    $this->assertText($captcha_text, 'First view of new form session should have CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(TRUE);
     $captcha_sid_initial = $this->getCaptchaSidFromForm();
 
     // Try to with wrong user name and password, but correct CAPTCHA.
@@ -664,24 +705,188 @@ class CapchaPersistenceTestCase extends 
     );
     $this->drupalPost(NULL, $edit, t('Log in'));
     // Check that there was no error message for the CAPTCHA.
-    $this->assertNoText(t('The answer you entered for the CAPTCHA was not correct.'),
-      'CAPTCHA answer should be correct', 'CAPTCHA');
+    $this->assertCaptchaResponseAccepted();
     // There shouldn't be a CAPTCHA on the new form.
-    $this->assertNoText($captcha_text, 'Updated form (after correct CAPTCHA) should have no CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertPreservedCsid($captcha_sid_initial);
 
     // Start a new form instance/session
     $this->drupalGet('node');
     $this->drupalGet('user');
-    $this->assertNoText($captcha_text, 'A new session should also have no CAPTCHA anymore.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertDifferentCsid($captcha_sid_initial);
 
     // Check another form
     $this->drupalGet('user/register');
-    $this->assertNoText($captcha_text, 'Another form should also have no CAPTCHA.', 'CAPTCHA');
+    $this->assertCaptchaPresence(FALSE);
     $this->assertDifferentCsid($captcha_sid_initial);
   }
 
+}
+
+
+
+
+class CaptchaSessionReuseAttackCase extends CaptchaBaseWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('CAPTCHA session reuse attack tests'),
+      'description' => t('Testing of the protection against CAPTCHA session reuse attacks.'),
+      'group' => t('CAPTCHA'),
+    );
+  }
+
+  /**
+   * Assert that the CAPTCHA session ID reuse attack was detected.
+   */
+  protected function assertCaptchaSessionIdReuseAttackDetection() {
+    $this->assertText(
+      t(CAPTCHA_SESSION_REUSE_ATTACK_ERROR_MESSAGE),
+      'CAPTCHA session ID reuse attack should be detected.',
+      'CAPTCHA'
+    );
+    // There should be an error message about wrong response.
+    $this->assertText(
+      t(CAPTCHA_WRONG_RESPONSE_ERROR_MESSAGE),
+      'CAPTCHA response should flagged as wrong.',
+      'CAPTCHA'
+    );
+
+  }
+
+  function testCaptchaSessionReuseAttackDetectionOnCommentPreview() {
+    // Set Test CAPTCHA on comment form.
+    captcha_set_form_id_setting('comment_form', 'captcha/Math');
+    variable_set('captcha_persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE);
+
+    // Log in as normal user.
+    $this->drupalLogin($this->normal_user);
+
+    // Go to comment form of commentable node.
+    $this->drupalGet('comment/reply/' . $this->comment_node->nid);
+    $this->assertCaptchaPresence(TRUE);
+
+    // Get CAPTCHA session ID and solution of the challenge.
+    $captcha_sid = $this->getCaptchaSidFromForm();
+    $captcha_token = $this->getCaptchaTokenFromForm();
+    $solution = $this->getMathCaptchaSolutionFromForm();
+
+    // Post the form with the solution.
+    $edit = array(
+      'subject' => $this->randomName(32, 'subject_'),
+      'comment' =>  $this->randomName(256, 'comment_body'),
+      'captcha_response' => $solution,
+    );
+    $this->drupalPost(NULL, $edit, t('Preview'));
+    // Answer should be accepted and further CAPTCHA ommitted.
+    $this->assertCaptchaResponseAccepted();
+    $this->assertCaptchaPresence(FALSE);
+
+    // Post a new comment, reusing the previous CAPTCHA session.
+    $edit = array(
+      'subject' => $this->randomName(32, 'subject_'),
+      'comment' =>  $this->randomName(256, 'comment_body'),
+      'captcha_sid' => $captcha_sid,
+      'captcha_token' => $captcha_token,
+      'captcha_response' => $solution,
+    );
+    $this->drupalPost('comment/reply/' . $this->comment_node->nid, $edit, t('Preview'));
+    // CAPTCHA session reuse attack should be detected.
+    $this->assertCaptchaSessionIdReuseAttackDetection();
+    // There should be a CAPTCHA.
+    $this->assertCaptchaPresence(TRUE);
+
+  }
+
+  function testCaptchaSessionReuseAttackDetectionOnNodeForm() {
+    // Set CAPTCHA on page form.
+    captcha_set_form_id_setting('page_node_form', 'captcha/Math');
+    variable_set('captcha_persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE);
+
+    // Log in as normal user.
+    $this->drupalLogin($this->normal_user);
+
+    // Go to node add form.
+    $this->drupalGet('node/add/page');
+    $this->assertCaptchaPresence(TRUE);
+
+    // Get CAPTCHA session ID and solution of the challenge.
+    $captcha_sid = $this->getCaptchaSidFromForm();
+    $captcha_token = $this->getCaptchaTokenFromForm();
+    $solution = $this->getMathCaptchaSolutionFromForm();
+
+    // Page settings to post, with correct CAPTCHA answer.
+    $edit = array(
+      'title'     => $this->randomName(8, 'node_title_'),
+      'body'      => $this->randomName(32, 'node_body_'),
+      'captcha_response' => $solution,
+    );
+    // Preview the node
+    $this->drupalPost(NULL, $edit, t('Preview'));
+    // Answer should be accepted.
+    $this->assertCaptchaResponseAccepted();
+    // Check that there is no CAPTCHA after preview.
+    $this->assertCaptchaPresence(FALSE);
+
+    // Post a new comment, reusing the previous CAPTCHA session.
+    $edit = array(
+      'title'     => $this->randomName(8, 'node_title_'),
+      'body'      => $this->randomName(32, 'node_body_'),
+      'captcha_sid' => $captcha_sid,
+      'captcha_token' => $captcha_token,
+      'captcha_response' => $solution,
+    );
+    $this->drupalPost('node/add/page', $edit, t('Preview'));
+    // CAPTCHA session reuse attack should be detected.
+    $this->assertCaptchaSessionIdReuseAttackDetection();
+    // There should be a CAPTCHA.
+    $this->assertCaptchaPresence(TRUE);
+
+  }
+
+  function testCaptchaSessionReuseAttackDetectionOnLoginForm() {
+    // Set CAPTCHA on login form.
+    captcha_set_form_id_setting('user_login', 'captcha/Math');
+    variable_set('captcha_persistence', CAPTCHA_PERSISTENCE_SKIP_ONCE_SUCCESSFUL_PER_FORM_INSTANCE);
+
+    // Go to log in form.
+    $this->drupalGet('user');
+    $this->assertCaptchaPresence(TRUE);
+
+    // Get CAPTCHA session ID and solution of the challenge.
+    $captcha_sid = $this->getCaptchaSidFromForm();
+    $captcha_token = $this->getCaptchaTokenFromForm();
+    $solution = $this->getMathCaptchaSolutionFromForm();
+
+    // Log in through form.
+    $edit = array(
+      'name' => $this->normal_user->name,
+      'pass' => $this->normal_user->pass_raw,
+      'captcha_response' => $solution,
+    );
+    $this->drupalPost(NULL, $edit, t('Log in'));
+    $this->assertCaptchaResponseAccepted();
+    $this->assertCaptchaPresence(FALSE);
+    // If a "log out" link appears on the page, it is almost certainly because
+    // the login was successful.
+    $pass = $this->assertLink(t('Log out'), 0, t('User %name successfully logged in.', array('%name' => $this->normal_user->name)), t('User login'));
+
+    // Log out again.
+    $this->drupalLogout();
+
+    // Try to log in again, reusing the previous CAPTCHA session.
+    $edit += array(
+      'captcha_sid' => $captcha_sid,
+      'captcha_token' => $captcha_token,
+    );
+    $this->drupalPost('user', $edit, t('Log in'));
+    // CAPTCHA session reuse attack should be detected.
+    $this->assertCaptchaSessionIdReuseAttackDetection();
+    // There should be a CAPTCHA.
+    $this->assertCaptchaPresence(TRUE);
+
+  }
 
 
 }
