[wp-trac] [WordPress Trac] #48356: wp_create_nonce(...) and check_ajax_referer(...) fails on the 2nd AJAX call if that is two-action AJAX with AJAX-LOGIN as the first action

WordPress Trac noreply at wordpress.org
Thu Oct 17 13:32:07 UTC 2019


#48356: wp_create_nonce(...) and check_ajax_referer(...) fails on the 2nd AJAX call
if that is two-action AJAX with AJAX-LOGIN as the first action
--------------------------+-----------------------------
 Reporter:  KestutisIT    |      Owner:  (none)
     Type:  defect (bug)  |     Status:  new
 Priority:  normal        |  Milestone:  Awaiting Review
Component:  General       |    Version:  5.2.3
 Severity:  major         |   Keywords:  needs-patch
  Focuses:  javascript    |
--------------------------+-----------------------------
 So if I generate a new `SpecialPluginGlobals['AJAX_SECURITY']` variable
 with `wp_create_nonce(..)` and register it as a global JavaScript variable
 in AssetController.php:
 {{{#!php
 <?php
         wp_localize_script('special-plugin-main', 'SpecialPluginGlobals',
 array(
             'AJAX_SECURITY' => wp_create_nonce('special-plugin-front-ajax-
 nonce'),
         ));
 }}}

 And then I Have "Already have an account? Please login" section in the
 page, where customers does login with it via class method call from
 `SpecialPluginMain.js`:


 {{{#!javascript
 // Dynamic variables
 if(typeof SpecialPluginGlobals === "undefined")
 {
     // The values here will come from WordPress script localizations,
     // but in case if they wouldn't, we have a backup initializer below
     var SpecialPluginGlobals = {};
 }

 // NOTE: For object-oriented language experience, this variable name
 should always match current file name
 var FleetManagementMain = {
     globals: FleetManagementGlobals,
     lang: FleetManagementLang,
     vars: FleetManagementVars,

     // <...> Some other JS class methods

     doLogin: function ()
     {
         jQuery.ajax({
             type: 'POST',
             dataType: 'json',
             url: this.globals['LOGIN_AJAX_URL'],
             data:
             {
                 'action': 'ajaxlogin', //calls wp_ajax_nopriv_ajaxlogin
                 'login_account_id_or_email': jQuery('.special-plugin-
 search-summary-table form.login-form input.login-account-id-or-
 email').val(),
                 'login_password': jQuery('.special-plugin-search-summary-
 table form.login-form input.login-password').val(),
                 'ajax_security': this.globals['AJAX_SECURITY']
             },
             success: function(data)
             {
                 jQuery('.special-plugin-search-summary-table .login-
 result').text(data.message);
                 if (data.loggedIn === true)
                 {
                     // NOTE: We set customers dropdown immediately
                     //console.log('Logged in successfully!');

                     // Clear the guest customer lookup section
                     jQuery('.special-plugin-search-summary-table .guest-
 customer-lookup-section').empty();
                     jQuery('.special-plugin-search-summary-table .login-
 form').empty();

                     SpecialPluginMain.setCustomersDropdownHTML();
                 }
             }
         });
     },

     // <...> Other JS class methods like 'setCustomersDropdownHTML()'
 };
 }}}

 Then the system will crash on second Ajax call which tries to pull the
 list (make an HTML dropdown) of all customers with that Account ID
 (WordPress user id).

 I can sure probably try to send back the new $ajaxNounce after login via
 JSON, but that is breaking the idea of why why having this AJAX_NOUNCE -
 to check the source autenthity, and if the script returns it via JS Ajax
 call then it is a security issue.

 And problem here is that, the WordPress changes the NOUNCE after user
 get's logged in.
 This should not be happening first of all, as the purpose of NONCE, first
 of all is to check the SOURCE AUTHENTICITY that the request really came
 from the same domain, and is not for password checking - as then it is
 super easy to break the WordPress security if that is the password
 mechanism.

 So problem is in this method in WordPress core, that it creates different
 nonces for GUEST and LOGGED_IN user, while there should be a way to create
 a nonce that would work for both states. As well as the function is self-
 describing to check the nonce only, and should not do anything else, what
 would be an unexpected behavior:

 {{{#!php
 <?php
         function wp_create_nonce( $action = -1 ) {
                 $user = wp_get_current_user();
                 $uid  = (int) $user->ID;
                 if ( ! $uid ) {
                         /** This filter is documented in wp-
 includes/pluggable.php */
                         $uid = apply_filters( 'nonce_user_logged_out',
 $uid, $action );
                 }

                 $token = wp_get_session_token();
                 $i     = wp_nonce_tick();

                 return substr( wp_hash( $i . '|' . $action . '|' . $uid .
 '|' . $token, 'nonce' ), -12, 10 );
         }
 }}}


 The very bad hack for this issue is demonstrated below (creating a new
 nonce and sending it via AJAX RESPONSE:
 {{{#!php
 <?php
 final class MainController
 {
     /**
      * Starts the plug-in main functionality
      */
     public function runOnInit()
     {
          // <..> Other code

          // User hook
          if (!is_user_logged_in())
          {
               // Enable the user with no privileges to run ajax_login() in
 AJAX
               add_action('wp_ajax_nopriv_ajaxlogin', array($this,
 'ajaxLogin'));
          }
     }

     /**
      *
      */
     public function ajaxLogin()
     {
         if($this->canProcess)
         {
             try
             {
                 // Assign routing to conf, only if it is not yet assigned
                 $conf = $this->conf();
                 // Load the language file, only if it is not yet loaded
                 $lang = $this->i18n();

                 // First check the nonce, if it fails the function will
 break
                 check_ajax_referer('special-plugin-front-ajax-nonce',
 'ajax_security');

                 // Nonce is checked, get the POST data and sign user on
                 $params = array();
                 $paramWP_UserIdOrEmail =
 isset($_POST['login_account_id_or_email']) ?
 $_POST['login_account_id_or_email'] : 0;
                 $userField = is_email($paramWP_UserIdOrEmail) ? 'email' :
 'id';
                 $params['user_login'] = get_user_by($userField,
 $paramWP_UserIdOrEmail)->user_login;
                 $params['user_password'] = isset($_POST['login_password'])
 ? $_POST['login_password'] : '';
                 $params['remember'] = TRUE;

                 $objWPSignOn = wp_signon($params, FALSE);
                 if (is_wp_error($objWPSignOn))
                 {
                     $jsonParams = array(
                         'loggedIn' => FALSE,
                         'message'=>
 $lang->escJS('LANG_USER_ACCOUNT_ID_OR_PASSWORD_ERROR_TEXT'),
                     );
                 } else
                     {
                     $jsonParams = array(
                         'loggedIn' => TRUE,
                         'message'=>
 $lang->escJS('LANG_USER_SUCCESSFULLY_LOGGED_IN_TEXT'),
                         'new_ajax_nonce' => wp_create_nonce('special-
 plugin-front-ajax-nonce'),
                     );
                 }

                 echo json_encode($jsonParams);
                 die();
             } catch (\Exception $e)
             {
                 $this->processError(__FUNCTION__, $e->getMessage());
             }
         }
     }

     // <..> Other methods
 }
 }}}


 For testing, if needed, the code of all above (a bit older version, and
 without the AJAX LOGIN part) is available via:
 https://wordpress.org/plugins/expandable-faq/
 Or
 https://github.com/SolidMVC/ExpandableFAQ/tree/master/Controllers

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/48356>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list