#620280 by Damien Tournoud: Protect the logout link against CSRF attacks.

From: Damien Tournoud <damien@tournoud.net>


---
 logintoboggan.install |    8 ++++
 logintoboggan.module  |   89 ++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 96 insertions(+), 1 deletions(-)

diff --git logintoboggan.install logintoboggan.install
index 246d3b7..f0bdbee 100644
--- logintoboggan.install
+++ logintoboggan.install
@@ -157,6 +157,14 @@ function logintoboggan_update_6001() {
 }
 
 /**
+ * Enable CSRF protection of the logout link.
+ */
+function logintoboggan_update_6002() {
+  logintoboggan_rebuild_links();
+  return array();
+}
+
+/**
  * Implementation of hook_uninstall().
  */
 function logintoboggan_uninstall() {
diff --git logintoboggan.module logintoboggan.module
index 40c205b..9b8b623 100755
--- logintoboggan.module
+++ logintoboggan.module
@@ -806,8 +806,45 @@ function logintoboggan_main_settings(&$form_state) {
     '#default_value' => variable_get('logintoboggan_minimum_password_length', 0),
     '#description' => t('LoginToboggan automatically performs basic password validation for illegal characters. If you would additionally like to have a mimimum password length requirement, select the length here, or set to \'None\' for no password length validation.')
   );
+  $form['other']['logintoboggan_logout_csrf_protection'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Protect the logout link'),
+    '#default_value' => variable_get('logintoboggan_logout_csrf_protection', TRUE),
+    '#description' => t('Add a token to the logout link to protect it against CSRF attacks.')
+  );
+
+  $form = system_settings_form($form);
+  $form['#submit'][] = 'logintoboggan_rebuild_links';
 
-  return system_settings_form($form);
+  return $form;
+}
+
+/**
+ * Submit callback for the logintoboggan main settings form.
+ *
+ * @see logintoboggan_main_settings()
+ */
+function logintoboggan_main_settings_submit($form, &$form_state) {
+  if ($form_state['values']['logintoboggan_logout_csrf_protection'] != $form['other']['logintoboggan_logout_csrf_protection']['#default_value']) {
+    // If the logout CSRF protection changed, rebuild the links and the router.
+    logintoboggan_rebuild_links();
+  }
+}
+
+/**
+ * Perform the operations required when changing the logout link CSRF protection settings.
+ */
+function logintoboggan_rebuild_links() {
+  // First rebuild the router, so that logintoboggan_menu_alter() can kick-in.
+  menu_rebuild();
+
+  // Then resave all the menu links that have a path = 'logout', so that
+  // logintoboggan_menu_link_alter() can kick-in.
+  $res = db_query("SELECT ml.mlid FROM {menu_links} ml WHERE ml.link_path = '%s'", 'logout');
+  while ($menu_link = db_fetch_array($res)) {
+    $menu_link = menu_link_load($menu_link['mlid']);
+    menu_link_save($menu_link);
+  }
 }
 
 function logintoboggan_denied() {
@@ -1311,6 +1348,56 @@ function logintoboggan_mail($key, &$message, $params) {
 }
 
 /**
+ * Implementation of hook_menu_alter().
+ *
+ * Add a validation function to the 'logout' router.
+ *
+ * Note: we cannot directly add a wildcard to this menu item, because
+ * menu links with wildcards are not supported by Drupal 6.
+ * @see http://drupal.org/node/204077
+ */
+function logintoboggan_menu_alter(&$items) {
+  if (variable_get('logintoboggan_logout_csrf_protection', TRUE) && isset($items['logout'])) {
+    $items['logout']['page callback'] = 'logintoboggan_logout';
+  }
+}
+
+/**
+ * Implementation of hook_menu_link_alter().
+ *
+ * Mark any menu link with path 'logout' as alterable,
+ * so that we can modify their path dynamically.
+ */
+function logintoboggan_menu_link_alter(&$item) {
+  if (variable_get('logintoboggan_logout_csrf_protection', TRUE) && ($item['path'] == 'logout')) {
+    $item['options']['alter'] = TRUE;
+  }
+}
+
+/**
+ * Implementation of hook_translated_menu_link_alter().
+ *
+ * Dynamically modify the path of the 'logout' link.
+ */
+function logintoboggan_translated_menu_link_alter(&$item, $map) {
+  if (variable_get('logintoboggan_logout_csrf_protection', TRUE) && ($item['href'] == 'logout')) {
+    $item['href'] .= '/' . drupal_get_token();
+  }
+}
+
+/**
+ * Page callback; logout the current user with token validation.
+ */
+function logintoboggan_logout($token = NULL) {
+  if (!variable_get('logintoboggan_logout_csrf_protection', TRUE) || ($token && drupal_valid_token($token))) {
+    user_logout();
+  }
+  else {
+    drupal_not_found();
+  }
+}
+
+/**
  *
  * THEME FUNCTIONS!
  *
