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 18 Jun 2010 21:36:32 -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 18 Jun 2010 21:36:33 -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,6 +204,14 @@ 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']); @@ -214,6 +223,13 @@ function captcha_process($element, $edit 'captcha_sid' => $captcha_sid, ); + // Store the current CAPTCHA info in $form_state, so we can + // reuse it after a form rebuild in _captcha_get_posted_captcha_info(). + $form_state['captcha_info'] = array( + 'captcha_sid' => $captcha_sid, + 'form_id' => $posted_form_id, + ); + 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 +425,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,8 +450,13 @@ 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) { + $token_check_required = TRUE; // Get the post data from where we can find it. - if (isset($element['#post']) && count($element['#post'])) { + if (isset($form_state['captcha_info'])) { + $post_data = $form_state['captcha_info']; + $token_check_required = FALSE; + } + else if (isset($element['#post']) && count($element['#post'])) { $post_data = $element['#post']; } else if (isset($form_state['clicked_button']['#post'])) { @@ -447,9 +469,22 @@ 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; + + // Check if the posted CAPTCHA token is valid for the posted CAPTCHA session. + if ($token_check_required && $posted_captcha_sid != NULL) { + $posted_captcha_token = isset($post_data['captcha_token']) ? $post_data['captcha_token'] : 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", $captcha_sid); + } return array($posted_form_id, $posted_captcha_sid); } @@ -462,22 +497,16 @@ function _captcha_get_posted_captcha_inf * files). */ function captcha_validate($element, &$form_state) { + $captcha_info = $element['#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));