Commit ab155e63 authored by soxofaan's avatar soxofaan

#810534 by soxofaan: fixed CAPTCHA session reuse hack

parent aaae9ab8
......@@ -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;
}
......@@ -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.
......@@ -198,22 +199,43 @@ function captcha_process($element, $edit, &$form_state, $complete_form) {
}
// Store CAPTCHA session ID as hidden field.
// Strictly speaking, it is not necessary to send the CAPTCHA session id
// with the form, as the one time CAPTCHA token (see lower) is enough.
// However, we still send it along, because it can help debugging
// problems on live sites with only access to the markup.
$element['captcha_sid'] = array(
'#type' => 'hidden',
'#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 +431,7 @@ function captcha_validate_case_insensitive_ignore_spaces($solution, $response) {
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,23 +456,51 @@ function captcha_validate_case_insensitive_ignore_spaces($solution, $response) {
* 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($element['#post']) && count($element['#post'])) {
$post_data = $element['#post'];
}
else if (isset($form_state['clicked_button']['#post'])) {
$post_data = $form_state['clicked_button']['#post'];
}
else if (isset($form_state['buttons']['button']['0']['#post'])) {
$post_data = $form_state['buttons']['button']['0']['#post'];
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 {
// No posted CAPTCHA info found (probably a fresh form).
$post_data = array();
// 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'];
}
else if (isset($form_state['clicked_button']['#post'])) {
$post_data = $form_state['clicked_button']['#post'];
}
else if (isset($form_state['buttons']['button']['0']['#post'])) {
$post_data = $form_state['buttons']['button']['0']['#post'];
}
else {
// No posted CAPTCHA info found (probably a fresh form).
$post_data = array();
}
// 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']) ? (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 ID. Note that we could just check the validity of the CAPTCHA
// token and extract the CAPTCHA session ID from that (without looking at
// the actual posted CAPTCHA session ID). However, here we check both
// the posted CAPTCHA token and session ID: it is a bit more stringent
// and the database query should also be more efficient (because there is
// an index on the CAPTCHA session ID).
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 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;
return array($posted_form_id, $posted_captcha_sid);
}
......@@ -462,27 +513,23 @@ function _captcha_get_posted_captcha_info($element, $form_state) {
* 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).',
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment