Changeset 501
- Timestamp:
- Nov 16, 2014 11:07:01 AM (10 years ago)
- Location:
- trunk/lib
- Files:
-
- 2 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/lib/App.inc.php
r500 r501 80 80 'ssl_enabled' => false, 81 81 82 // Use CSRF tokens. 82 // Use CSRF tokens. See notes in the getCSRFToken() method. 83 83 'csrf_token_enabled' => true, 84 // Form tokens will expire after this duration, in seconds. 85 'csrf_token_timeout' => 259200, // 259200 seconds = 3 days. 84 86 'csrf_token_name' => 'csrf_token', 85 'csrf_token_timeout' => 86400, // In seconds. This causes form tokens to be unusable after this duration. This might only cause problems when opening forms in multiple tabs left open beyond the timeout duration. But usually their session will timeout first, and they'll receive new tokens when they load the form again..86 87 87 88 // HMAC signing method … … 1018 1019 // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken() 1019 1020 if ($this->getParam('csrf_token_enabled') && $include_csrf_token) { 1020 printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this-> recycleCSRFToken());1021 printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this->getCSRFToken()); 1021 1022 } 1022 1023 } … … 1041 1042 1042 1043 /* 1043 * Generate a csrf_token, saving it to the session and returning its value. 1044 * Generate a csrf_token if it doesn't exist or is expired, save it to the session and return its value. 1045 * Otherwise just return the current token. 1046 * Details on the synchronizer token pattern: 1044 1047 * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern 1045 * @access public1046 * @return string The new csrf_token.1047 * @author Quinn Comendant <quinn@strangecode.com>1048 * @version 1.01049 * @since 15 Nov 2014 17:53:511050 */1051 public function generateCSRFToken()1052 {1053 return $_SESSION['_app'][$this->_ns]['csrf_tokens'][] = addSignature(time(), null, 64);1054 }1055 1056 /*1057 * Update the csrf_token to a new value if it hasn't been set yet or has expired.1058 * Save the previous csrf_token in the session to ensure continuity of currently open sessions.1059 1048 * 1060 1049 * @access public 1061 * @return string The current csrf_token1050 * @return string The new or current csrf_token 1062 1051 * @author Quinn Comendant <quinn@strangecode.com> 1063 1052 * @version 1.0 1064 1053 * @since 15 Nov 2014 17:57:17 1065 1054 */ 1066 public function recycleCSRFToken() 1067 { 1068 if (!isset($_SESSION['_app'][$this->_ns]['csrf_tokens'])) { 1069 // No token yet; generate one and return it. 1070 $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array(); 1071 $return = $this->generateCSRFToken(); 1072 } 1073 if (removeSignature(end($_SESSION['_app'][$this->_ns]['csrf_tokens'])) + $this->getParam('csrf_token_timeout') < time()) { 1074 // Newest token is expired; prune array of tokens and generate new token. 1075 // We'll save the 10-most-recent tokens. This allows the user to submit up to 5 forms saved in previously opened tabs with expired tokens (loading a form will prune one token, and submitting a form will prune one token, thus 10 = 5). 1076 $_SESSION['_app'][$this->_ns]['csrf_tokens'] = array_slice($_SESSION['_app'][$this->_ns]['csrf_tokens'], -10, 10); 1077 $return = $this->generateCSRFToken(); 1055 public function getCSRFToken() 1056 { 1057 if (!isset($_SESSION['_app'][$this->_ns]['csrf_token']) || (removeSignature($_SESSION['_app'][$this->_ns]['csrf_token']) + $this->getParam('csrf_token_timeout') < time())) { 1058 // No token, or token is expired; generate one and return it. 1059 return $_SESSION['_app'][$this->_ns]['csrf_token'] = addSignature(time(), null, 64); 1078 1060 } 1079 1061 // Current token is not expired; return it. 1080 $return = end($_SESSION['_app'][$this->_ns]['csrf_tokens']); 1081 $app =& App::getInstance(); 1082 return $return; 1062 return $_SESSION['_app'][$this->_ns]['csrf_token']; 1083 1063 } 1084 1064 … … 1093 1073 * @since 15 Nov 2014 18:06:55 1094 1074 */ 1095 public function verifyCSRFToken($csrf_token) 1096 { 1097 $app =& App::getInstance(); 1075 public function verifyCSRFToken($user_submitted_csrf_token) 1076 { 1098 1077 1099 1078 if (!$this->getParam('csrf_token_enabled')) { 1100 $ app->logMsg(sprintf('%s method called, but csrf_token_enabled=false', __FUNCTION__), LOG_ERR, __FILE__, __LINE__);1101 return false;1102 } 1103 if ('' == trim($ csrf_token)) {1104 $ app->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__);1105 return false; 1106 } 1107 if (!verifySignature($ csrf_token, null, 64)) {1108 $ app->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $csrf_token), LOG_WARNING, __FILE__, __LINE__);1109 return false; 1110 } 1111 $ this->recycleCSRFToken();1112 if ( !in_array($csrf_token, $_SESSION['_app'][$this->_ns]['csrf_tokens'])) {1113 $ app->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_WARNING, __FILE__, __LINE__);1114 return false; 1115 } 1116 // $app->logMsg(sprintf('Verified token %s is in %s', $csrf_token, getDump($_SESSION['_app'][$this->_ns]['csrf_tokens'])), LOG_DEBUG, __FILE__, __LINE__);1079 $this->logMsg(sprintf('%s method called, but csrf_token_enabled=false', __FUNCTION__), LOG_ERR, __FILE__, __LINE__); 1080 return true; 1081 } 1082 if ('' == trim($user_submitted_csrf_token)) { 1083 $this->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__); 1084 return false; 1085 } 1086 if (!verifySignature($user_submitted_csrf_token, null, 64)) { 1087 $this->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $user_submitted_csrf_token), LOG_WARNING, __FILE__, __LINE__); 1088 return false; 1089 } 1090 $csrf_token = $this->getCSRFToken(); 1091 if ($user_submitted_csrf_token != $csrf_token) { 1092 $this->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $user_submitted_csrf_token, $csrf_token), LOG_WARNING, __FILE__, __LINE__); 1093 return false; 1094 } 1095 $this->logMsg(sprintf('Verified CSRF token %s is in %s', $user_submitted_csrf_token, $csrf_token), LOG_DEBUG, __FILE__, __LINE__); 1117 1096 return true; 1118 1097 } … … 1123 1102 * 1124 1103 * @access public 1125 * @param string $ csrf_token Thetoken to compare with the session token.1104 * @param string $user_submitted_csrf_token The user-submitted token to compare with the session token. 1126 1105 * @param string $message Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message. 1127 1106 * @param int $type The type of message: MSG_NOTICE, … … 1134 1113 * @since 15 Nov 2014 18:10:17 1135 1114 */ 1136 public function requireValidCSRFToken($csrf_token, $message=null, $type=MSG_NOTICE, $file=null, $line=null) 1137 { 1138 $app =& App::getInstance(); 1139 1140 if (!$this->verifyCSRFToken($csrf_token)) { 1141 $message = isset($message) ? $message : _("Something went wrong; please try again."); 1142 $app->raiseMsg($message, $type, $file, $line); 1143 $app->dieBoomerangURL(); 1115 public function requireValidCSRFToken($message=null, $type=MSG_NOTICE, $file=null, $line=null) 1116 { 1117 if (!$this->verifyCSRFToken(getFormData($this->getParam('csrf_token_name')))) { 1118 $message = isset($message) ? $message : _("Sorry, the form token expired. Please try again."); 1119 $this->raiseMsg($message, $type, $file, $line); 1120 $this->dieBoomerangURL(); 1144 1121 } 1145 1122 } -
trunk/lib/Auth_SQL.inc.php
r500 r501 32 32 class Auth_SQL { 33 33 34 // Available encryptiontypes for class Auth_SQL.34 // Available hash types for class Auth_SQL. 35 35 const ENCRYPT_PLAINTEXT = 1; 36 36 const ENCRYPT_CRYPT = 2; … … 68 68 'db_login_table' => 'user_login_tbl', 69 69 70 // The type of encryptionto use for passwords stored in the db_table. Use one of the Auth_SQL::ENCRYPT_* types specified above.71 // Hardened password hashes rely on the same key/salt being used to compare encryptions.70 // The type of hash to use for passwords stored in the db_table. Use one of the Auth_SQL::ENCRYPT_* types specified above. 71 // Hardened password hashes rely on the same key/salt being used to compare hashs. 72 72 // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change! 73 'encryption_type' => self::ENCRYPT_MD5, 73 'hash_type' => self::ENCRYPT_MD5, 74 'encryption_type' => null, // Backwards misnomer compatibility. 75 76 // Automatically update stored user hashes when the user next authenticates if the hash type changes (requires user_tbl with populated userpass_hashtype column). 77 'hash_type_autoupdate' => true, 74 78 75 79 // The URL to the login script. … … 257 261 public function setParam($params) 258 262 { 263 $app =& App::getInstance(); 264 259 265 if (isset($params['match_remote_ip_exempt_usernames'])) { 260 266 $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']); … … 263 269 $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']); 264 270 } 265 if (isset($params['encryption_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['encryption_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) { 271 if (isset($params['encryption_type'])) { 272 // Backwards misnomer compatibility. 273 $params['hash_type'] = $params['encryption_type']; 274 } 275 if (isset($params['hash_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['hash_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) { 266 276 // These hash types require the password_* userland lib in PHP < 5.5.0 267 277 $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php'; … … 269 279 include_once $pw_compat_lib; 270 280 } else { 271 $app =& App::getInstance(); 272 $app->logMsg(sprintf('Encryption type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1', $params['encryption_type']), LOG_ERR, __FILE__, __LINE__); 273 $params['encryption_type'] = self::ENCRYPT_SHA1; 274 } 281 $app->logMsg(sprintf('Hash type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__); 282 $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED; 283 } 284 } 285 if (isset($params['hash_type']) && !in_array($params['hash_type'], array(self::ENCRYPT_PLAINTEXT, self::ENCRYPT_CRYPT, self::ENCRYPT_SHA1, self::ENCRYPT_SHA1_HARDENED, self::ENCRYPT_MD5, self::ENCRYPT_MD5_HARDENED, self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) { 286 $app->logMsg(sprintf('Invalid hash type %s; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__); 287 $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED; 275 288 } 276 289 if (isset($params) && is_array($params)) { … … 393 406 } 394 407 395 if ($this->verifyPassword($password, $user_data['userpass'])) { 408 $old_hash_type = isset($user_data['userpass_hashtype']) && !empty($user_data['userpass_hashtype']) ? $user_data['userpass_hashtype'] : $this->getParam('hash_type'); 409 if ($this->verifyPassword($password, $user_data['userpass'], $old_hash_type)) { 396 410 $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__); 397 411 unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data. 412 if ($this->getParam('hash_type_autoupdate') && $old_hash_type != $this->getParam('hash_type')) { 413 // Let's update user's password hash to new type (just run setPassword with this authenticated passwordâŠ). 414 $this->setPassword($user_data['user_id'], $password); 415 $app->logMsg(sprintf('User %s password hash type updated from %s to %s', $username, $old_hash_type, $this->getParam('hash_type')), LOG_INFO, __FILE__, __LINE__); 416 } 398 417 return $user_data; 399 418 } … … 835 854 * 836 855 */ 837 public function encryptPassword($password, $salt=null )856 public function encryptPassword($password, $salt=null, $hash_type=null) 838 857 { 839 858 $app =& App::getInstance(); … … 841 860 $password = (string)$password; 842 861 843 // Existing password hashes rely on the same key/salt being used to compare encryptions.862 // Existing password hashes rely on the same key/salt being used to compare hashs. 844 863 // Don't change this (or the value applied to signing_key) unless you know existing hashes or signatures will not be affected! 845 864 $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81'; 846 865 847 switch ($this->_params['encryption_type']) { 866 $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type'); 867 868 switch ($hash_type) { 848 869 case self::ENCRYPT_PLAINTEXT : 849 870 $encrypted_password = $password; … … 886 907 887 908 default : 888 $app->logMsg(sprintf(' Authentication encrypt type specified is unrecognized: %s', $this->_params['encryption_type']), LOG_NOTICE, __FILE__, __LINE__);909 $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__); 889 910 return false; 890 break;891 911 } 892 912 893 913 // In case our hashing function returns 'false' or another empty value, bail out. 894 914 if ('' == trim((string)$encrypted_password)) { 895 $app->logMsg(sprintf('Invalid password hash returned ; check yo crypto!', null), LOG_ALERT, __FILE__, __LINE__);915 $app->logMsg(sprintf('Invalid password hash returned ("%s") for hash type %s; check yo crypto!', $encrypted_password, $hash_type), LOG_ALERT, __FILE__, __LINE__); 896 916 return false; 897 917 } … … 910 930 * @since 15 Nov 2014 21:37:28 911 931 */ 912 public function verifyPassword($password, $encrypted_password) 913 { 914 switch ($this->_params['encryption_type']) { 932 public function verifyPassword($password, $encrypted_password, $hash_type=null) 933 { 934 $app =& App::getInstance(); 935 936 $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type'); 937 938 switch ($hash_type) { 915 939 case self::ENCRYPT_CRYPT : 916 940 return $this->encryptPassword($password, $encrypted_password) == $encrypted_password; … … 928 952 return password_verify($password, $encrypted_password); 929 953 } 930 } 931 932 /** 933 * 934 */ 935 public function setPassword($user_id=null, $password) 954 955 $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__); 956 return false; 957 } 958 959 /** 960 * 961 */ 962 public function setPassword($user_id=null, $password, $hash_type=null) 936 963 { 937 964 $app =& App::getInstance(); … … 943 970 $user_id = isset($user_id) ? $user_id : $this->get('user_id'); 944 971 945 // Get old password. 946 $qid = $db->query(" 947 SELECT userpass 948 FROM " . $this->_params['db_table'] . " 949 WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' 950 "); 951 if (!list($old_encrypted_password) = mysql_fetch_row($qid)) { 952 $app->logMsg(sprintf('Cannot set password for nonexistent user_id %s', $user_id), LOG_WARNING, __FILE__, __LINE__); 953 return false; 954 } 955 956 // Compare old with new to ensure we're actually *changing* the password. 957 if ($this->verifyPassword($password, $old_encrypted_password)) { 958 $app->logMsg(sprintf('Not setting password: new is the same as old.', null), LOG_INFO, __FILE__, __LINE__); 959 return null; 960 } 972 // New hash type. 973 $hash_type = isset($hash_type) ? $hash_type : $this->getParam('hash_type'); 961 974 962 975 // Save the hash method used if a table exists for it. 963 $userpass_hashtype = '';976 $userpass_hashtype_clause = ''; 964 977 if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) { 965 $userpass_hashtype = ", userpass_hashtype = '" . $db->escapeString($this->getParam('encryption_type')) . "'";978 $userpass_hashtype_clause = ", userpass_hashtype = '" . $db->escapeString($hash_type) . "'"; 966 979 } 967 980 … … 969 982 $db->query(" 970 983 UPDATE " . $this->_params['db_table'] . " 971 SET userpass = '" . $db->escapeString($this->encryptPassword($password )) . "'972 $userpass_hashtype 984 SET userpass = '" . $db->escapeString($this->encryptPassword($password, null, $hash_type)) . "' 985 $userpass_hashtype_clause 973 986 WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "' 974 987 "); 975 988 976 989 if (mysql_affected_rows($db->getDBH()) != 1) { 977 $app->logMsg(sprintf('Failed to update password for user_id %s ', $user_id), LOG_WARNING, __FILE__, __LINE__);990 $app->logMsg(sprintf('Failed to update password for user_id %s (no affected rows)', $user_id), LOG_WARNING, __FILE__, __LINE__); 978 991 return false; 979 992 }
Note: See TracChangeset
for help on using the changeset viewer.