PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.0
Patchstack – WordPress & Plugins Security v2.3.0
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / login.php

login.php in Patchstack – WordPress & Plugins Security 2.3.0, at includes/login.php

465 lines 15.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used to alter anything related to the login page.
10 */
11 class P_Login extends P_Core {
12
13 /**
14 * Add the actions required to interact with the login process.
15 *
16 * @param Patchstack $core
17 * @return void
18 */
19 public function __construct( $core ) {
20 parent::__construct( $core );
21
22 if ( $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
23 return;
24 }
25
26 add_action( 'login_init', [ $this, 'add_captcha' ] );
27 add_action( 'login_init', [ $this, 'check_ipban' ] );
28 add_action( 'login_init', [ $this, 'check_logonhours' ] );
29 add_action( 'login_head', [ $this, 'add_captcha' ] );
30 add_action( 'login_enqueue_scripts', [ $this, 'login_enqueue_scripts' ], 1 );
31
32 // WooCommerce related functionality.
33 if ( class_exists( 'WooCommerce' ) ) {
34 add_action( 'woocommerce_login_form_start', [ $this, 'add_captcha' ] );
35 add_action( 'woocommerce_register_form_start', [ $this, 'add_captcha' ] );
36 add_action( 'wp_authenticate', [ $this, 'add_captcha' ] );
37 add_filter( 'woocommerce_process_registration_errors', [$this, 'general_captcha_check' ], 10, 1 );
38 add_action( 'woocommerce_before_lost_password_form', [ $this, 'add_captcha' ] );
39 add_action( 'lostpassword_post', [ $this, 'general_captcha_check' ], 1, 1 );
40 }
41
42 // 2FA related actions.
43 if ( $this->get_option( 'patchstack_login_2fa', 0 ) ) {
44 add_action( 'login_form', [ $this, 'tfa_login_form' ] );
45 add_action( 'authenticate', [ $this, 'tfa_authenticate' ], 30, 3 );
46 add_action( 'profile_personal_options', [ $this, 'tfa_profile_personal_options' ] );
47 add_action( 'personal_options', [ $this, 'tfa_personal_options' ] );
48 add_action( 'edit_user_profile_update', [ $this, 'tfa_options_update' ] );
49 add_action( 'personal_options_update', [ $this, 'tfa_personal_options_update' ] );
50 add_action( 'admin_enqueue_scripts', [ $this, 'tfa_admin_enqueue_scripts' ] );
51
52 // WooCommerce related functionality.
53 if ( class_exists( 'WooCommerce' ) ) {
54 add_action( 'woocommerce_login_form', [ $this, 'tfa_woocommerce_login_form' ] );
55 add_action( 'woocommerce_edit_account_form', [ $this, 'tfa_woocommerce_profile_personal_options' ] );
56 add_action( 'woocommerce_save_account_details_errors', [ $this, 'tfa_woocommerce_validate_tfa'], 10, 2 );
57 }
58 }
59 }
60
61 /**
62 * Register the Google reCAPTCHA JavaScript for the login area.
63 *
64 * @return void
65 */
66 public function login_enqueue_scripts() {
67 if ( $this->get_option( 'patchstack_captcha_login_form', false ) && $this->get_option( 'patchstack_captcha_type' ) != 'v3' && $this->get_option( 'patchstack_captcha_type' ) != 'turnstile' ) {
68 wp_enqueue_script( 'patchstack_captcha', 'https://www.google.com/recaptcha/api.js' );
69 }
70 }
71
72 /**
73 * Add the 2FA code to the login form.
74 *
75 * @return void
76 */
77 public function tfa_login_form() {
78 require_once dirname( __FILE__ ) . '/views/2fa-login-form.php';
79 }
80
81 /**
82 * Add the 2FA code to the WooCommerce login form.
83 *
84 * @return void
85 */
86 public function tfa_woocommerce_login_form() {
87 require_once dirname( __FILE__ ) . '/views/2fa-login-form-woocommerce.php';
88 }
89
90 /**
91 * Check the 2FA code, if 2FA is enabled for the user.
92 *
93 * @param object $user
94 * @param string $username
95 * @param string $password
96 * @return object|WP_User|WP_Error
97 */
98 public function tfa_authenticate( $user, $username = '', $password = '' ) {
99 if ( ! isset( $user->ID ) ) {
100 return $user;
101 }
102
103 // If we have a valid user object, check to see if the user has 2FA enabled.
104 $enabled = get_user_option( 'webarx_2fa_enabled', $user->ID );
105 if ( empty( $enabled ) ) {
106 return $user;
107 }
108
109 // If enabled, check to see if the verification code is being sent.
110 if ( ! isset( $_POST['patchstack_2fa'] ) || ( isset( $_POST['patchstack_2fa'] ) && $_POST['patchstack_2fa'] == '' ) ) {
111 return new WP_Error( 'patchstack_2fa_empty_code', esc_attr__( 'Please enter the 2FA authentication code that is generated on your device.', 'patchstack' ) );
112 }
113
114 // Verify the code.
115 require_once dirname( __FILE__ ) . '/2fa/rfc6238.php';
116 $secret = $this->tfa_get_secret( $user );
117 if ( ! TokenAuth6238::verify( $secret, trim( $_POST['patchstack_2fa'] ) ) ) {
118 return new WP_Error( 'patchstack_2fa_invalid_code', esc_attr__( 'The 2FA authentication code you entered is invalid.', 'patchstack' ) );
119 }
120
121 return $user;
122 }
123
124 /**
125 * Show the 2FA disable field to the admin.
126 *
127 * @param object $user
128 * @return void
129 */
130 public function tfa_personal_options( $user ) {
131 require_once dirname( __FILE__ ) . '/views/2fa-profile-configuration-admin.php';
132 }
133
134 /**
135 * Show the 2FA fields.
136 *
137 * @param object $user
138 * @return void
139 */
140 public function tfa_profile_personal_options( $user ) {
141 $secret = $this->tfa_get_secret( $user );
142 require_once dirname( __FILE__ ) . '/views/2fa-profile-configuration.php';
143 }
144
145 /**
146 * Show the 2FA fields.
147 *
148 * @param object $user
149 * @return void
150 */
151 public function tfa_woocommerce_profile_personal_options( $user ) {
152 $secret = $this->tfa_get_secret( $user );
153 require_once dirname( __FILE__ ) . '/views/2fa-profile-configuration-woocommerce.php';
154 }
155
156 /**
157 * Validate the 2FA connection of a WooCommerce customer.
158 *
159 * @param mixed $errors
160 * @param mixed $user
161 * @return void
162 */
163 public function tfa_woocommerce_validate_tfa( &$errors, &$user ) {
164 // If we have a valid user object, check to see if the user has 2FA enabled.
165 $enabled = get_user_option( 'webarx_2fa_enabled', $user->ID );
166 if ( $enabled || ! isset ( $_POST['patchstack_2fa_enabled'] ) ) {
167 $this->tfa_personal_options_update( $user->ID );
168 return;
169 }
170
171 // Verify the code.
172 require_once dirname( __FILE__ ) . '/2fa/rfc6238.php';
173 $secret = $this->tfa_get_secret( $user );
174 if ( ! TokenAuth6238::verify( $secret, trim( $_POST['patchstack_2fa_secretkey_verification'] ) ) ) {
175 wc_add_notice( __( 'The 2FA authentication code you entered is invalid.', 'patchstack' ), 'error' );
176 return;
177 }
178
179 $this->tfa_personal_options_update( $user->ID );
180 }
181
182 /**
183 * Update the 2FA fields.
184 *
185 * @param integer $user_id
186 * @return void
187 */
188 public function tfa_personal_options_update( $user_id ) {
189 update_user_option( $user_id, 'webarx_2fa_enabled', ! empty( $_POST['patchstack_2fa_enabled'] ), true );
190 }
191
192 /**
193 * Update the 2FA fields on admin.
194 *
195 * @param integer $user_id
196 * @return void
197 */
198 public function tfa_options_update( $user_id ) {
199 update_user_option( $user_id, 'webarx_2fa_enabled', ! empty( $_POST['patchstack_2fa_enabled'] ), true );
200 }
201
202 /**
203 * Add the QRCode image generator JavaScript library.
204 *
205 * @return void
206 */
207 public function tfa_admin_enqueue_scripts() {
208 wp_register_script( 'patchstack_qrcode', $this->plugin->url . '/assets/js/qrcode.min.js', [], $this->plugin->version );
209 wp_enqueue_script( 'patchstack_qrcode' );
210 }
211
212 /**
213 * In case of legacy conditions, we encrypt the secret key and then store it.
214 *
215 * @return string
216 */
217 private function tfa_get_secret( $user ) {
218 $secret = get_user_option( 'webarx_2fa_secretkey', $user->ID );
219
220 // If user has no secret key set yet, generate one.
221 if ( empty( $secret ) || strlen( $secret ) === 16 ) {
222 if ( empty( $secret ) ) {
223 require_once dirname( __FILE__ ) . '/2fa/rfc6238.php';
224 $secret = TokenAuth6238::generateRandomClue();
225 }
226
227 $enc = $this->encrypt( $secret );
228 update_user_option( $user->ID, 'webarx_2fa_secretkey', $enc['cipher'], true );
229 update_user_option( $user->ID, 'webarx_2fa_secretkey_nonce', $enc['nonce'], true );
230 } else {
231 $nonce = get_user_option( 'webarx_2fa_secretkey_nonce', $user->ID );
232 $secret = $this->decrypt( $secret, $nonce );
233 }
234
235 return $secret;
236 }
237
238 /**
239 * Check if the IP address is banned from attempting to guess passwords.
240 *
241 * @return void
242 */
243 public function check_ipban() {
244 if ( is_user_logged_in() || ! $this->get_option( 'patchstack_block_bruteforce_ips', 0 ) ) {
245 return;
246 }
247
248 // Check if the users IP address is whitelisted.
249 $ip = $this->get_ip();
250 if ( $this->plugin->ban->is_ip_whitelisted( $ip ) ) {
251 return;
252 }
253
254 // Calculate block time.
255 $minutes = (int) $this->get_option( 'patchstack_anti_bruteforce_minutes', 30 );
256 $timeout = (int) $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 );
257 if ( empty( $minutes ) || empty( $timeout ) ) {
258 $time = 30 + 60;
259 } else {
260 $time = $minutes + $timeout;
261 }
262
263 // Check if X failed login attempts were made.
264 global $wpdb;
265 $results = $wpdb->get_results(
266 $wpdb->prepare( 'SELECT COUNT(*) AS numIps FROM ' . $wpdb->prefix . "patchstack_event_log WHERE ip = '%s' AND action = 'failed login' AND date >= ('" . current_time( 'mysql' ) . "' - INTERVAL %d MINUTE)", [ $ip, $time ] ),
267 OBJECT
268 );
269
270 // Determine the number of attempts.
271 if ( ! isset( $results, $results[0], $results[0]->numIps ) ) {
272 $num = 0;
273 } else {
274 $num = $results[0]->numIps;
275 }
276
277 // Block the user?
278 if ( $num >= $this->get_option( 'patchstack_anti_bruteforce_attempts', 10 ) ) {
279 $this->plugin->firewall_base->display_error_page( 24 );
280 }
281 }
282
283 /**
284 * If logon hours are set, check the current time and allow or disallow the user
285 * to login depending on the settings.
286 *
287 * @return void
288 */
289 public function check_logonhours() {
290 if ( ! $this->get_option( 'patchstack_login_time_block', 0 ) || is_user_logged_in() || $this->get_option( 'patchstack_login_time_start', '00:00' ) == $this->get_option( 'patchstack_login_time_end', '23:59' ) ) {
291 return;
292 }
293 $block = true;
294
295 // Current time.
296 $hour = current_time( 'G' );
297 $min = current_time( 'i' );
298 $stamp_current = current_time( 'U' );
299
300 // Get time start.
301 $start = explode( ':', str_replace( '00', '0', $this->get_option( 'patchstack_login_time_start', '00:00' ) ) );
302 if ( count( $start ) != 2 ) {
303 return;
304 }
305 $stamp_start = strtotime( current_time( 'Y-m-d' ) . ' ' . $this->get_option( 'patchstack_login_time_start', '00:00' ) . ':00' );
306 $start[0] = (int) $start[0];
307 $start[1] = (int) $start[1];
308
309 // Get time end.
310 $end = explode( ':', str_replace( '00', '0', $this->get_option( 'patchstack_login_time_end', '23:59' ) ) );
311 if ( count( $end ) != 2 ) {
312 return;
313 }
314 $stamp_end = strtotime( current_time( 'Y-m-d' ) . ' ' . $this->get_option( 'patchstack_login_time_end', '00:00' ) . ':00' );
315 $end[0] = (int) $end[0];
316 $end[1] = (int) $end[1];
317
318 // If begin time is earlier than end time.
319 if ( $start[0] <= $end[0] && $stamp_current >= $stamp_start && $stamp_current <= $stamp_end ) {
320 $block = false;
321 }
322
323 // If begin time is later than end time.
324 if ( $start[0] > $end[0] && ( $hour >= $start[0] || $hour <= $end[0] ) ) {
325 $block = false;
326
327 if ( ( $hour == $start[0] && $min < $start[1] ) || ( $hour == $end[0] && $min > $end[1] ) ) {
328 $block = true;
329 }
330 }
331
332 // Block the user?
333 if ( $block ) {
334 wp_die( esc_attr__( 'Access to the login page has been restricted due to set logon hours.', 'patchstack' ), esc_attr__( 'Login Disallowed', 'patchstack' ) );
335 }
336 }
337
338 /**
339 * Determine if we should inject reCAPTCHA into certain pages.
340 *
341 * @return void
342 */
343 public function add_captcha() {
344 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
345 case 'v2':
346 $public = $this->get_option( 'patchstack_captcha_public_key', '' );
347 $private = $this->get_option( 'patchstack_captcha_private_key', '' );
348 break;
349 case 'invisible':
350 $public = $this->get_option( 'patchstack_captcha_public_key_v3', '' );
351 $private = $this->get_option( 'patchstack_captcha_private_key_v3', '' );
352 break;
353 case 'v3':
354 $public = $this->get_option( 'patchstack_captcha_public_key_v3_new', '' );
355 $private = $this->get_option( 'patchstack_captcha_private_key_v3_new', '' );
356 break;
357 case 'turnstile':
358 $public = $this->get_option( 'patchstack_captcha_public_key_turnstile', '' );
359 $private = $this->get_option( 'patchstack_captcha_private_key_turnstile', '' );
360 break;
361 default:
362 return;
363 break;
364 }
365
366 // Make sure that the keys are set.
367 if ( $public == '' || $private == '' ) {
368 return;
369 }
370
371 // reCAPTCHA on the login page.
372 if ( $this->get_option( 'patchstack_captcha_login_form' ) ) {
373 add_filter( 'login_form', [ $this->plugin->hardening, 'captcha_display' ] );
374 add_filter( 'woocommerce_login_form', [ $this->plugin->hardening, 'captcha_display' ] );
375 add_filter( 'wp_authenticate_user', [ $this, 'login_captcha_check' ], 10, 2 );
376 }
377
378 // reCAPTCHA on the registration form.
379 if ( $this->get_option( 'patchstack_captcha_registration_form' ) ) {
380 add_action( 'register_form', [ $this->plugin->hardening, 'captcha_display' ] );
381 add_action( 'woocommerce_register_form', [ $this->plugin->hardening, 'captcha_display' ] );
382 add_action( 'registration_errors', [ $this, 'general_captcha_check' ] );
383 }
384
385 // reCAPTCHA on the reset password form.
386 if ( $this->get_option( 'patchstack_captcha_reset_pwd_form' ) ) {
387 add_action( 'lostpassword_form', [ $this->plugin->hardening, 'captcha_display' ] );
388 add_action( 'woocommerce_lostpassword_form', [ $this->plugin->hardening, 'captcha_display' ] );
389 add_action( 'allow_password_reset', [ $this, 'general_captcha_check' ] );
390 }
391 }
392
393 /**
394 * Check reCAPTCHA upon login.
395 *
396 * @param string $user
397 * @param string $password
398 * @return WP_User|WP_Error
399 */
400 public function login_captcha_check( $user, $password ) {
401 $result = $this->plugin->hardening->captcha_check();
402
403 if ( ! $result['response'] ) {
404 if ( $result['reason'] === 'ERROR_NO_KEYS' ) {
405 return $user;
406 }
407 $error_message = sprintf( '<strong>%s</strong>: %s', 'Error', esc_attr__( 'You have entered an incorrect reCAPTCHA value.', 'patchstack' ) );
408
409 if ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) {
410 wp_clear_auth_cookie();
411 return new WP_Error( 'patchstack_error', $error_message );
412 }
413
414 if ( isset( $_REQUEST['log'], $_REQUEST['pwd'] ) ) {
415 return new WP_Error( 'patchstack_error', $error_message );
416 }
417 } else {
418 return $user;
419 }
420 }
421
422 /**
423 * Check reCAPTCHA upon login.
424 *
425 * @param string $user
426 * @param string $password
427 * @return WP_User|WP_Error
428 */
429 public function login_captcha_check_woocommerce( $error, $username, $password, $email ) {
430 $result = $this->plugin->hardening->captcha_check();
431
432 if ( $result['response'] || $result['reason'] == 'ERROR_NO_KEYS' ) {
433 return $error;
434 }
435
436 if ( ! is_wp_error( $error ) ) {
437 $error = new WP_Error();
438 }
439
440 $error->add( 'patchstack_error', 'ERROR' . ':&nbsp;' . esc_attr__( 'You have entered an incorrect reCAPTCHA value.', 'patchstack' ) );
441 return $error;
442 }
443
444 /**
445 * Captcha check for the register or lost password form.
446 *
447 * @param mixed|WP_Error $error
448 * @return WP_Error
449 */
450 public function general_captcha_check( $error ) {
451 $result = $this->plugin->hardening->captcha_check();
452
453 if ( $result['response'] || $result['reason'] == 'ERROR_NO_KEYS' ) {
454 return $error;
455 }
456
457 if ( ! is_wp_error( $error ) ) {
458 $error = new WP_Error();
459 }
460
461 $error->add( 'patchstack_error', 'ERROR' . ':&nbsp;' . esc_attr__( 'You have entered an incorrect reCAPTCHA value.', 'patchstack' ) );
462 return $error;
463 }
464 }
465