diff --git a/assets/js/lsx-login.min.js b/assets/js/lsx-login.min.js index 7ffe897..b48702c 100644 --- a/assets/js/lsx-login.min.js +++ b/assets/js/lsx-login.min.js @@ -1 +1 @@ -function getQueryString(){var s=!1,i={},e=null,a=location.search.substring(1);arguments.length>0&&arguments[0].length>1&&(s=arguments[0]);for(var r=/([^&=]+)=([^&]*)/g;e=r.exec(a);){if(!1!==s&&decodeURIComponent(e[1])===s)return decodeURIComponent(e[2]);!1===s&&(i[decodeURIComponent(e[1])]=decodeURIComponent(e[2]))}return!1===s?i:null}jQuery(document).ready(function(s){var i=!1;(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)))&&(i=!0),0==i?(jQuery(".loginform").submit(function(i){i.preventDefault();var e=s(this);s(this).find("div.error").each(function(i){s(this).remove()});var a=s(this).find("input.user_login").val();if(""==a)return s(this).find("input.user_login").parent("p").append('
'+a.message+"
"),e.find("input.user_login").addClass("error")):"2"==a.success?(e.find(".login-username").append(''+a.message+"
"))}}}),!1}),jQuery('form[name="resetpassform"]').submit(function(i){i.preventDefault();var e=s(this);s(this).find(".error").each(function(i){s(this).remove()});var a=s(this).find("input.pass1").val();if(""==a)return s(this).find("input.pass1").parent("p").append(''+a.message+"
"),e.find("input.pass2").addClass("error")):"1"==a.success&&(e.find("p").each(function(i){s(this).remove()}),e.append(''+a.message+"
"))}}})}),s("input.user_login , input.user_pass, input.pass1, input.pass2").click(function(i){s(this).removeClass("error"),s(this).parent("p").find(".error").remove()}),s(window).bind("load",function(){var i=getQueryString("action"),e=getQueryString("key"),a=getQueryString("login");s("body").hasClass("page-my-account")||null===i||"rp"!==i||null===e||null===a||(s("#login-modal").modal("show"),s('a[href="#tab-lost-password"]').trigger("click"))})):jQuery(".loginform").attr("action","/wp-login.php"),void 0===jQuery().dropdown&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.dropdown.js",dataType:"script",cache:!0}),void 0===jQuery().modal&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.modal.js",dataType:"script",cache:!0}),void 0===jQuery().tab&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.tab.js",dataType:"script",cache:!0})}); \ No newline at end of file +function getQueryString(){var s=!1,e={},i=null,a=location.search.substring(1);arguments.length>0&&arguments[0].length>1&&(s=arguments[0]);for(var n=/([^&=]+)=([^&]*)/g;i=n.exec(a);){if(!1!==s&&decodeURIComponent(i[1])===s)return decodeURIComponent(i[2]);!1===s&&(e[decodeURIComponent(i[1])]=decodeURIComponent(i[2]))}return!1===s?e:null}jQuery(document).ready(function(s){var e=!1;(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)))&&(e=!0),0==e?(jQuery(".loginform").submit(function(e){e.preventDefault();var i=s(this);s(this).find("div.error").each(function(e){s(this).remove()});var a=s(this).find("input.user_login").val();if(""==a)return s(this).find("input.user_login").parent("p").append(''+a.message+"
"),i.find("input.user_login").addClass("error")):"2"==a.success?(i.find(".login-username").append(''+a.message+"
"))}}}),!1}),jQuery('form[name="resetpassform"]').submit(function(e){e.preventDefault();var i=s(this);s(this).find(".error").each(function(e){s(this).remove()});var a=s(this).find("input.pass1").val();if(""==a)return s(this).find("input.pass1").parent("p").append(''+a.message+"
"),i.find("input.pass2").addClass("error")):"1"==a.success&&(i.find("p").each(function(e){s(this).remove()}),i.append(''+a.message+"
"))}}})}),s("input.user_login , input.user_pass, input.pass1, input.pass2").click(function(e){s(this).removeClass("error"),s(this).parent("p").find(".error").remove()}),s(window).bind("load",function(){var e=getQueryString("action"),i=getQueryString("key"),a=getQueryString("login");s("body").hasClass("page-my-account")||null===e||"rp"!==e||null===i||null===a||(s("#login-modal").modal("show"),s('a[href="#tab-lost-password"]').trigger("click"))})):jQuery(".loginform").attr("action","/wp-login.php"),void 0===jQuery().dropdown&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.dropdown.js",dataType:"script",cache:!0}),void 0===jQuery().modal&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.modal.js",dataType:"script",cache:!0}),void 0===jQuery().tab&&jQuery.ajax({url:lsx_login_params.theme_url+"assets/js/vendor/bootstrap.tab.js",dataType:"script",cache:!0})}); \ No newline at end of file diff --git a/assets/js/src/lsx-login.js b/assets/js/src/lsx-login.js index b863a0f..8c6a3bc 100644 --- a/assets/js/src/lsx-login.js +++ b/assets/js/src/lsx-login.js @@ -61,6 +61,7 @@ jQuery(document).ready(function($) { var redirect = $(this).find('input[name="redirect_to"]' ).val(); var params = { action: 'lsx_login', + nonce: lsx_login_params.nonce, log: username, pwd: password, rememberme: remember, @@ -130,6 +131,7 @@ jQuery(document).ready(function($) { var params = { action: 'lsx_reset', + nonce: lsx_login_params.nonce, log: username, method: 'reset' }; @@ -213,6 +215,7 @@ jQuery(document).ready(function($) { var params = { action: 'lsx_reset_confirmed', + nonce: lsx_login_params.nonce, pass1: pass1, pass2: pass2, key: key, diff --git a/classes/class-lsx-login.php b/classes/class-lsx-login.php index cdcb67a..696ad50 100644 --- a/classes/class-lsx-login.php +++ b/classes/class-lsx-login.php @@ -20,6 +20,27 @@ class LSX_Login { */ public $plugin_slug = 'lsx-login'; + /** + * Nonce action shared by the front-end AJAX handlers. + * + * @var string + */ + const NONCE_ACTION = 'lsx_login_ajax'; + + /** + * Password reset requests allowed per IP per window. + * + * @var int + */ + const RESET_RATE_LIMIT = 5; + + /** + * Length of the password reset rate limit window, in seconds. + * + * @var int + */ + const RESET_RATE_WINDOW = 900; + /** * Plugin options. * @@ -158,6 +179,7 @@ public function scripts() { $params = array( 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'empty_username' => __( 'The username field is empty.', 'lsx-login' ), 'empty_password' => __( 'The password field is empty.', 'lsx-login' ), 'empty_reset' => __( 'Enter a username or e-mail address.', 'lsx-login' ), @@ -311,6 +333,53 @@ public function login_form() { } } + /** + * Verify the nonce on a front-end AJAX request. + * + * These handlers are registered on wp_ajax_nopriv_*, so they are + * reachable unauthenticated and the nonce is the only request-origin + * check available. On a failure the request is ended here. + */ + private function verify_ajax_nonce() { + if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) { + status_header( 403 ); + echo wp_json_encode( + array( + 'success' => 2, + 'message' => __( 'Your session has expired. Please reload the page and try again.', 'lsx-login' ), + ) + ); + die(); + } + } + + /** + * Throttle password reset requests by client IP. + * + * Without this an unauthenticated caller can invalidate any user's + * stored activation key, and send them mail, as fast as it can post. + * + * @return bool True while the caller is under the limit. + */ + private function reset_rate_limit_ok() { + $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; + + if ( '' === $ip ) { + return true; + } + + $key = 'lsx_login_reset_' . md5( $ip ); + $count = (int) get_transient( $key ); + + if ( $count >= self::RESET_RATE_LIMIT ) { + return false; + } + + set_transient( $key, $count + 1, self::RESET_RATE_WINDOW ); + + return true; + } + /** * generate the login form * @@ -342,6 +411,8 @@ public function logout_layout_filter( $layout ) { * */ public function do_ajax_login() { + $this->verify_ajax_nonce(); + if(isset($_POST['method']) && 'login' == $_POST['method']){ $result = array(); @@ -352,8 +423,11 @@ public function do_ajax_login() { $result['success'] = 1; }else{ $result['success'] = 3; - //TODO Fix this encapsulation - $result['message'] = __('The password you entered for the username '.$_POST['log'].' is incorrect.','lsx-login'); + $result['message'] = sprintf( + /* translators: %s: the username that was submitted. */ + __( 'The password you entered for the username %s is incorrect.', 'lsx-login' ), + esc_html( sanitize_user( wp_unslash( $_POST['log'] ), true ) ) + ); } }else{ $result['success'] = 2; @@ -373,6 +447,19 @@ public function do_ajax_login() { public function do_ajax_reset() { global $wpdb; + $this->verify_ajax_nonce(); + + if ( ! $this->reset_rate_limit_ok() ) { + status_header( 429 ); + echo wp_json_encode( + array( + 'success' => 2, + 'message' => __( 'Too many password reset requests. Please try again later.', 'lsx-login' ), + ) + ); + die(); + } + if(isset($_POST['method']) && 'reset' == $_POST['method']){ $result = array(); @@ -434,7 +521,6 @@ public function do_ajax_reset() { }else{ $result['success'] = 1; $result['message'] = __('Check your e-mail for the confirmation link.','lsx-login'); - $result['email'] = $message; } } @@ -479,8 +565,23 @@ public function is_password_confirmed() { * */ public function do_ajax_reset_confirmed() { + $this->verify_ajax_nonce(); + if(isset($_POST['key']) && isset($_POST['login']) && isset($_POST['pass1']) && isset($_POST['pass2']) ){ $result = array(); + + // The two password fields were never compared server-side, so a + // mismatch the front-end failed to catch silently set pass1. + if ( $_POST['pass1'] !== $_POST['pass2'] ) { + echo wp_json_encode( + array( + 'success' => 2, + 'message' => __( 'Passwords do not match', 'lsx-login' ), + ) + ); + die(); + } + $user = check_password_reset_key( $_POST['key'], $_POST['login'] ); if(!is_wp_error($user)){ diff --git a/templates/content-password-reset.php b/templates/content-password-reset.php index 65b2e5f..b3320bd 100644 --- a/templates/content-password-reset.php +++ b/templates/content-password-reset.php @@ -11,8 +11,8 @@ if(isset($_GET['action']) && 'rp' === $_GET['action'] && !is_wp_error($is_key_confirmed)){ ?>