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); + + } }