Got initial CSRF token framework implemented and sample implementation added in Special:Logout; removing Javascript compression engine from aggressive_optimize_html() and instead calling JavascriptCompressor class from js-compressor.php
authorDan
Sat, 07 Jun 2008 12:46:18 -0400
changeset 562 75df0b2c596c
parent 561 e53cf8b1d942
child 563 0103428e2179
Got initial CSRF token framework implemented and sample implementation added in Special:Logout; removing Javascript compression engine from aggressive_optimize_html() and instead calling JavascriptCompressor class from js-compressor.php
includes/clientside/static/faders.js
includes/functions.php
language/english/user.json
plugins/SpecialUserFuncs.php
--- a/includes/clientside/static/faders.js	Sat Jun 07 12:43:57 2008 -0400
+++ b/includes/clientside/static/faders.js	Sat Jun 07 12:46:18 2008 -0400
@@ -788,7 +788,7 @@
   var mb = new MessageBox(MB_YESNO|MB_ICONQUESTION, $lang.get('user_logout_confirm_title'), $lang.get('user_logout_confirm_body'));
   mb.onclick['Yes'] = function()
     {
-      window.location = makeUrlNS('Special', 'Logout/' + title);
+      window.location = makeUrlNS('Special', 'Logout/' + csrf_token + '/' + title);
     }
 }
 
--- a/includes/functions.php	Sat Jun 07 12:43:57 2008 -0400
+++ b/includes/functions.php	Sat Jun 07 12:46:18 2008 -0400
@@ -391,6 +391,91 @@
 
 }
 
+/**
+ * Generates a confirmation form if a CSRF check fails. Will terminate execution.
+ */
+
+function csrf_confirm_form()
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  global $lang;
+  
+  // If the token was overridden with the correct one, the user confirmed the action using this form. Continue exec.
+  if ( isset($_POST['cstok']) || isset($_GET ['cstok']) )
+  {
+    // using the if() check makes sure that the token isn't in a cookie, since $_REQUEST includes $_COOKIE.
+    $token_check =& $_REQUEST['cstok'];
+    if ( $token_check === $session->csrf_token )
+    {
+      // overridden token matches, continue exec
+      return true;
+    }
+  }
+  
+  $template->tpl_strings['PAGE_NAME'] = htmlspecialchars($lang->get('user_csrf_confirm_title'));
+  $template->header();
+  
+  // initial info
+  echo '<p>' . $lang->get('user_csrf_confirm_body') . '</p>';
+  
+  // start form
+  $form_method = ( empty($_POST) ) ? 'get' : 'post';
+  echo '<form action="' . htmlspecialchars($_SERVER['REQUEST_URI']) . '" method="' . $form_method . '" enctype="multipart/form-data">';
+  
+  echo '<fieldset enano:expand="closed">';
+  echo '<legend>' . $lang->get('user_csrf_confirm_btn_viewrequest') . '</legend><div>';
+  
+  if ( empty($_POST) )
+  {
+    // GET request
+    echo csrf_confirm_get_recursive();
+  }
+  else
+  {
+    // POST request
+    echo csrf_confirm_post_recursive();
+  }
+  echo '</div></fieldset>';
+  // insert the right CSRF token
+  echo '<input type="hidden" name="cstok" value="' . $session->csrf_token . '" />';
+  echo '<p><input type="submit" value="' . $lang->get('user_csrf_confirm_btn_continue') . '" /></p>';
+  echo '</form>';
+  
+  $template->footer();
+  
+  exit;
+}
+
+function csrf_confirm_get_recursive($_inner = false, $pfx = false, $data = false)
+{
+  // make posted arrays work right
+  if ( !$data )
+    ( $_inner == 'post' ) ? $data =& $_POST : $data =& $_GET;
+  foreach ( $data as $key => $value )
+  {
+    $pfx_this = ( empty($pfx) ) ? $key : "{$pfx}[{$key}]";
+    if ( is_array($value) )
+    {
+      csrf_confirm_get_recursive(true, $pfx_this, $value);
+    }
+    else if ( empty($value) )
+    {
+      echo htmlspecialchars($pfx_this . " = <nil>") . "<br />\n";
+      echo '<input type="hidden" name="' . htmlspecialchars($pfx_this) . '" value="" />';
+    }
+    else
+    {
+      echo htmlspecialchars($pfx_this . " = " . $value) . "<br />\n";
+      echo '<input type="hidden" name="' . htmlspecialchars($pfx_this) . '" value="' . htmlspecialchars($value) . '" />';
+    }
+  }
+}
+
+function csrf_confirm_post_recursive()
+{
+  csrf_confirm_get_recursive('post');
+}
+
 // Removed wikiFormat() from here, replaced with RenderMan::render
 
 /**
@@ -2894,6 +2979,8 @@
   
   // Optimize (but don't obfuscate) Javascript
   preg_match_all('/<script([ ]+.*?)?>(.*?)(\]\]>)?<\/script>/is', $html, $jscript);
+  require_once(ENANO_ROOT . '/includes/js-compressor.php');
+  $jsc = new JavascriptCompressor();
   
   // list of Javascript reserved words - from about.com
   $reserved_words = array('abstract', 'as', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'continue', 'const', 'debugger', 'default', 'delete', 'do',
@@ -2910,51 +2997,12 @@
     
     // echo('<pre>' . "-----------------------------------------------------------------------------\n" . htmlspecialchars($js) . '</pre>');
     
-    // for line optimization, explode it
-    $particles = explode("\n", $js);
-    
-    foreach ( $particles as $j => $atom )
-    {
-      // Remove comments
-      $atom = preg_replace('#\/\/(.+)#i', '', $atom);
-      
-      $atom = trim($atom);
-      if ( empty($atom) )
-        unset($particles[$j]);
-      else
-        $particles[$j] = $atom;
-    }
-    
-    $js = implode("\n", $particles);
-    
-    $js = preg_replace('#/\*(.*?)\*/#s', '', $js);
-    
-    // find all semicolons and then linebreaks, and replace with a single semicolon
-    $js = str_replace(";\n", ';', $js);
-    
-    // starting braces
-    $js = preg_replace('/\{([\s]+)/m', '{', $js);
-    $js = str_replace(")\n{", '){', $js);
-    
-    // ending braces (tricky)
-    $js = preg_replace('/\}([^;])/m', '};\\1', $js);
-    
-    // other rules
-    $js = str_replace("};\n", "};", $js);
-    $js = str_replace(",\n", ',', $js);
-    $js = str_replace("[\n", '[', $js);
-    $js = str_replace("]\n", ']', $js);
-    $js = str_replace("\n}", '}', $js);
-    
-    // newlines immediately before reserved words
-    $js = preg_replace("/(\)|;)\n$reserved_words/is", '\\1\\2', $js);
-    
-    // fix for firefox issue
-    $js = preg_replace('/\};([\s]*)(else|\))/i', '}\\2', $js);
+    $js = $jsc->getClean($js);
     
     $replacement = "<script{$jscript[1][$i]}>/* <![CDATA[ */ $js /* ]]> */</script>";
     // apply changes
     $html = str_replace($jscript[0][$i], $replacement, $html);
+     
   }
   
   // Re-insert untouchable tags
--- a/language/english/user.json	Sat Jun 07 12:43:57 2008 -0400
+++ b/language/english/user.json	Sat Jun 07 12:46:18 2008 -0400
@@ -90,6 +90,7 @@
       logout_confirm_body: 'If you log out, you will no longer be able to access your user preferences, your private messages, or certain areas of this site until you log in again.',
       logout_confirm_title_elev: 'Are you sure you want to de-authenticate?',
       logout_confirm_body_elev: 'If you de-authenticate, you will no longer be able to use the administration panel until you re-authenticate again. You may do so at any time using the Administration button on the sidebar.',
+      logout_confirm_btn_logout: 'Log Out',
       logout_err_title: 'An error occurred during the logout process.',
       // Unused at this point
       logout_err_not_loggedin: 'You don\'t seem to be logged in.',
@@ -226,6 +227,12 @@
       
       autofill_heading_suggestions: 'Username suggestions',
       autofill_msg_no_suggestions: 'No suggestions',
+      
+      // CSRF confirmation form
+      csrf_confirm_title: 'Invalid form confirmation key',
+      csrf_confirm_body: 'Your browser sent an invalid confirmation key for a form. Your session may have expired, or you may have been redirected here from a remote site in an attack known as Cross-Site Request Forgery (CSRF). If you are sure you want to continue with this action, you may click the button below. Otherwise, return to the main page and do not proceed.',
+      csrf_confirm_btn_viewrequest: 'View request and form data',
+      csrf_confirm_btn_continue: 'Continue',
     },
     usercp: {
       // Meta
--- a/plugins/SpecialUserFuncs.php	Sat Jun 07 12:43:57 2008 -0400
+++ b/plugins/SpecialUserFuncs.php	Sat Jun 07 12:46:18 2008 -0400
@@ -5,7 +5,7 @@
   "Plugin URI"   : "http://enanocms.org/",
   "Description"  : "plugin_specialuserfuncs_desc",
   "Author"       : "Dan Fuhry",
-  "Version"      : "1.1.3",
+  "Version"      : "1.1.4",
   "Author URI"   : "http://enanocms.org/"
 }
 **!*/
@@ -226,6 +226,12 @@
       case 'key_not_found':
         $errstring = $lang->get('user_err_key_not_found');
         break;
+      case 'ERR_DH_KEY_NOT_FOUND':
+        $errstring = $lang->get('user_err_dh_key_not_found') . " -- {$__login_status['debug']}";
+        break;
+      case 'ERR_DH_KEY_NOT_INTEGER':
+        $errstring = $lang->get('user_err_dh_key_not_numeric');
+        break;
       case 'key_wrong_length':
         $errstring = $lang->get('user_err_key_wrong_length');
         break;
@@ -252,7 +258,7 @@
           $attempts = $__login_status['lockout_threshold'];
         
         $server_time = time();
-        $time_rem = ( $__login_status['lockout_last_time'] == time() ) ? $__login_status['lockout_duration'] : $__login_status['lockout_duration'] - round( ( $server_time - $__login_status['lockout_last_time'] ) / 60 );
+        $time_rem = ( intval(@$__login_status['lockout_last_time']) == time() ) ? $__login_status['lockout_duration'] : $__login_status['lockout_duration'] - round( ( $server_time - $__login_status['lockout_last_time'] ) / 60 );
         if ( $time_rem < 1 )
           $time_rem = $__login_status['lockout_duration'];
         
@@ -452,9 +458,8 @@
   }
   if ( isset($_GET['act']) && $_GET['act'] == 'ajaxlogin' )
   {
-    die('This version of the Enano LoginAPI is deprecated. Please use the action.json method instead.');
-    $db->close();
-    exit;
+    echo 'This version of the Enano LoginAPI is deprecated. Please use the action.json method instead.';
+    return true;
   }
   if(isset($_POST['login']))
   {
@@ -480,7 +485,12 @@
       $dh_public = $_POST['dh_public_key'];
       if ( !preg_match('/^[0-9]+$/', $dh_public) )
       {
-        die_semicritical('DiffieHellman error', 'Public key not integer: ' . $dh_public);
+        $__login_status = array(
+          'success' => false,
+          'error' => 'ERR_DH_KEY_NOT_INTEGER',
+          'debug' => "public key: $dh_public"
+        );
+        return false;
       }
       $q = $db->sql_query('SELECT private_key, key_id FROM ' . table_prefix . "diffiehellman WHERE public_key = '$dh_public';");
       if ( !$q )
@@ -488,7 +498,12 @@
       
       if ( $db->numrows() < 1 )
       {
-        die_semicritical('DiffieHellman error', 'ERR_DH_KEY_NOT_FOUND');
+        $__login_status = array(
+          'success' => false,
+          'error' => 'ERR_DH_KEY_NOT_FOUND',
+          'debug' => "public key: $dh_public"
+        );
+        return false;
       }
       
       list($dh_private, $dh_key_id) = $db->fetchrow_num();
@@ -508,7 +523,12 @@
       $dh_hash = $_POST['crypt_key'];
       if ( $dh_secret_check !== $dh_hash )
       {
-        die_semicritical('DiffieHellman error', 'ERR_DH_HASH_NO_MATCH');
+        $__login_status = array(
+          'success' => false,
+          'error' => 'ERR_DH_HASH_NO_MATCH',
+          'debug' => "dh_secret_check = $dh_secret_check\ndh_hash_input = $dh_hash"
+        );
+        return false;
       }
       
       // All good! Generate the AES key
@@ -581,18 +601,28 @@
   exit;
 }
 
-function page_Special_Logout() {
+function page_Special_Logout()
+{
   global $db, $session, $paths, $template, $plugins; // Common objects
   global $lang;
+  
   if ( !$session->user_logged_in )
     $paths->main_page();
   
+  $token = $paths->getParam(0);
+  if ( $token !== $session->csrf_token )
+  {
+    csrf_confirm_form();
+  }
+  
   $l = $session->logout();
   if ( $l == 'success' )
   {
     $url = makeUrl(getConfig('main_page'), false, true);
-    if ( $pi = $paths->getAllParams() )
+    if ( $paths->getParam(1) )
     {
+      $pi = explode('/', $paths->getAllParams());
+      $pi = implode('/', array_values(array_slice($pi, 1)));
       list($pid, $ns) = RenderMan::strToPageID($pi);
       $perms = $session->fetch_page_acl($pid, $ns);
       if ( $perms->get_permissions('read') )
@@ -600,7 +630,7 @@
         $url = makeUrl($pi, false, true);
       }
     }
-    redirect($url, $lang->get('user_logout_success_title'), $lang->get('user_logout_success_body'), 4);
+    redirect($url, $lang->get('user_logout_success_title'), $lang->get('user_logout_success_body'), 3);
   }
   $template->header();
   echo '<h3>' . $lang->get('user_logout_err_title') . '</h3>';
@@ -2027,10 +2057,14 @@
   }
   
   $timestamp = enano_date('D, j M Y H:i:s T', $lang_local->lang_timestamp);
+  // generate expires header
+  $expires = date('r', mktime(-1, -1, -1, -1, -1, intval(date('y'))+1));
+
   header("Last-Modified: $timestamp");
   header("Date: $timestamp");
   header("ETag: \"$etag\"");
   header('Content-type: text/javascript');
+  header("Expires: $expires");
   
   $lang_local->fetch();
   echo "if ( typeof(enano_lang) != 'object' )
@@ -2108,9 +2142,6 @@
     }
     fclose($fh);
     
-    gzip_output();
-    
-    return true;
   }
   return true;
 }