Changeset 501


Ignore:
Timestamp:
Nov 16, 2014 11:07:01 AM (10 years ago)
Author:
anonymous
Message:

Optimizing auth and csrf token.

Location:
trunk/lib
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/lib/App.inc.php

    r500 r501  
    8080        'ssl_enabled' => false,
    8181
    82         // Use CSRF tokens.
     82        // Use CSRF tokens. See notes in the getCSRFToken() method.
    8383        'csrf_token_enabled' => true,
     84        // Form tokens will expire after this duration, in seconds.
     85        'csrf_token_timeout' => 259200, // 259200 seconds = 3 days.
    8486        '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..
    8687
    8788        // HMAC signing method
     
    10181019        // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
    10191020        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());
    10211022        }
    10221023    }
     
    10411042
    10421043    /*
    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:
    10441047    * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern
    1045     * @access   public
    1046     * @return   string  The new csrf_token.
    1047     * @author   Quinn Comendant <quinn@strangecode.com>
    1048     * @version  1.0
    1049     * @since    15 Nov 2014 17:53:51
    1050     */
    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.
    10591048    *
    10601049    * @access   public
    1061     * @return   string The current csrf_token
     1050    * @return   string The new or current csrf_token
    10621051    * @author   Quinn Comendant <quinn@strangecode.com>
    10631052    * @version  1.0
    10641053    * @since    15 Nov 2014 17:57:17
    10651054    */
    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);
    10781060        }
    10791061        // 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'];
    10831063    }
    10841064
     
    10931073    * @since    15 Nov 2014 18:06:55
    10941074    */
    1095     public function verifyCSRFToken($csrf_token)
    1096     {
    1097         $app =& App::getInstance();
     1075    public function verifyCSRFToken($user_submitted_csrf_token)
     1076    {
    10981077
    10991078        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__);
    11171096        return true;
    11181097    }
     
    11231102    *
    11241103    * @access   public
    1125     * @param    string  $csrf_token The token to compare with the session token.
     1104    * @param    string  $user_submitted_csrf_token The user-submitted token to compare with the session token.
    11261105    * @param    string  $message    Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message.
    11271106    * @param    int    $type    The type of message: MSG_NOTICE,
     
    11341113    * @since    15 Nov 2014 18:10:17
    11351114    */
    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();
    11441121        }
    11451122    }
  • trunk/lib/Auth_SQL.inc.php

    r500 r501  
    3232class Auth_SQL {
    3333
    34     // Available encryption types for class Auth_SQL.
     34    // Available hash types for class Auth_SQL.
    3535    const ENCRYPT_PLAINTEXT = 1;
    3636    const ENCRYPT_CRYPT = 2;
     
    6868        'db_login_table' => 'user_login_tbl',
    6969
    70         // The type of encryption 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 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.
    7272        // 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,
    7478
    7579        // The URL to the login script.
     
    257261    public function setParam($params)
    258262    {
     263        $app =& App::getInstance();
     264
    259265        if (isset($params['match_remote_ip_exempt_usernames'])) {
    260266            $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']);
     
    263269            $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
    264270        }
    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))) {
    266276            // These hash types require the password_* userland lib in PHP < 5.5.0
    267277            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
     
    269279                include_once $pw_compat_lib;
    270280            } 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;
    275288        }
    276289        if (isset($params) && is_array($params)) {
     
    393406        }
    394407
    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)) {
    396410            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
    397411            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            }
    398417            return $user_data;
    399418        }
     
    835854     *
    836855     */
    837     public function encryptPassword($password, $salt=null)
     856    public function encryptPassword($password, $salt=null, $hash_type=null)
    838857    {
    839858        $app =& App::getInstance();
     
    841860        $password = (string)$password;
    842861
    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.
    844863        // Don't change this (or the value applied to signing_key) unless you know existing hashes or signatures will not be affected!
    845864        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
    846865
    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) {
    848869        case self::ENCRYPT_PLAINTEXT :
    849870            $encrypted_password = $password;
     
    886907
    887908        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__);
    889910            return false;
    890             break;
    891911        }
    892912
    893913        // In case our hashing function returns 'false' or another empty value, bail out.
    894914        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__);
    896916            return false;
    897917        }
     
    910930    * @since    15 Nov 2014 21:37:28
    911931    */
    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) {
    915939        case self::ENCRYPT_CRYPT :
    916940            return $this->encryptPassword($password, $encrypted_password) == $encrypted_password;
     
    928952            return password_verify($password, $encrypted_password);
    929953        }
    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)
    936963    {
    937964        $app =& App::getInstance();
     
    943970        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
    944971
    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');
    961974
    962975        // Save the hash method used if a table exists for it.
    963         $userpass_hashtype = '';
     976        $userpass_hashtype_clause = '';
    964977        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) . "'";
    966979        }
    967980
     
    969982        $db->query("
    970983            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
    973986            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
    974987        ");
    975988
    976989        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__);
    978991            return false;
    979992        }
Note: See TracChangeset for help on using the changeset viewer.