PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.13
Patchstack – WordPress & Plugins Security v2.2.13
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.2.13, at includes/login.php

355 lines 11.5 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 // 2FA related actions.
33 if ( $this->get_option( 'patchstack_login_2fa', 0 ) ) {
34 add_action( 'login_form', [ $this, 'tfa_login_form' ] );
35 add_action( 'authenticate', [ $this, 'tfa_authenticate' ], 30, 3 );
36 add_action( 'profile_personal_options', [ $this, 'tfa_profile_personal_options' ] );
37 add_action( 'personal_options_update', [ $this, 'tfa_personal_options_update' ] );
38 add_action( 'admin_enqueue_scripts', [ $this, 'tfa_admin_enqueue_scripts' ] );
39 }
40 }
41
42 /**
43 * Register the Google reCAPTCHA JavaScript for the login area.
44 *
45 * @return void
46 */
47 public function login_enqueue_scripts() {
48 if ( $this->get_option( 'patchstack_captcha_login_form', false ) && $this->get_option( 'patchstack_captcha_type' ) != 'v3' && $this->get_option( 'patchstack_captcha_type' ) != 'turnstile' ) {
49 wp_enqueue_script( 'patchstack_captcha', 'https://www.google.com/recaptcha/api.js' );
50 }
51 }
52
53 /**
54 * Add the 2FA code to the login form.
55 *
56 * @return void
57 */
58 public function tfa_login_form() {
59 require_once dirname( __FILE__ ) . '/views/2fa-login-form.php';
60 }
61
62 /**
63 * Check the 2FA code, if 2FA is enabled for the user.
64 *
65 * @param object $user
66 * @param string $username
67 * @param string $password
68 * @return object|WP_User|WP_Error
69 */
70 public function tfa_authenticate( $user, $username = '', $password = '' ) {
71 if ( ! isset( $user->ID ) ) {
72 return $user;
73 }
74
75 // If we have a valid user object, check to see if the user has 2FA enabled.
76 $enabled = get_user_option( 'webarx_2fa_enabled', $user->ID );
77 if ( empty( $enabled ) ) {
78 return $user;
79 }
80
81 // If enabled, check to see if the verification code is being sent.
82 if ( ! isset( $_POST['patchstack_2fa'] ) || ( isset( $_POST['patchstack_2fa'] ) && $_POST['patchstack_2fa'] == '' ) ) {
83 return new WP_Error( 'patchstack_2fa_empty_code', esc_attr__( 'Please enter the 2FA authentication code that is generated on your device.', 'patchstack' ) );
84 }
85
86 // Verify the code.
87 require_once dirname( __FILE__ ) . '/2fa/rfc6238.php';
88 $secret = $this->tfa_get_secret( $user );
89 if ( ! TokenAuth6238::verify( $secret, trim( $_POST['patchstack_2fa'] ) ) ) {
90 return new WP_Error( 'patchstack_2fa_invalid_code', esc_attr__( 'The 2FA authentication code you entered is invalid.', 'patchstack' ) );
91 }
92
93 return $user;
94 }
95
96 /**
97 * Show the 2FA fields.
98 *
99 * @param object $user
100 * @return void
101 */
102 public function tfa_profile_personal_options( $user ) {
103 $secret = $this->tfa_get_secret( $user );
104 require_once dirname( __FILE__ ) . '/views/2fa-profile-configuration.php';
105 }
106
107 /**
108 * Update the 2FA fields.
109 *
110 * @param integer $user_id
111 * @return void
112 */
113 public function tfa_personal_options_update( $user_id ) {
114 update_user_option( $user_id, 'webarx_2fa_enabled', ! empty( $_POST['patchstack_2fa_enabled'] ), true );
115 }
116
117 /**
118 * Add the QRCode image generator JavaScript library.
119 *
120 * @return void
121 */
122 public function tfa_admin_enqueue_scripts() {
123 wp_register_script( 'patchstack_qrcode', $this->plugin->url . '/assets/js/qrcode.min.js', [], $this->plugin->version );
124 wp_enqueue_script( 'patchstack_qrcode' );
125 }
126
127 /**
128 * In case of legacy conditions, we encrypt the secret key and then store it.
129 *
130 * @return string
131 */
132 private function tfa_get_secret( $user ) {
133 $secret = get_user_option( 'webarx_2fa_secretkey', $user->ID );
134
135 // If user has no secret key set yet, generate one.
136 if ( empty( $secret ) || strlen( $secret ) === 16 ) {
137 if ( empty( $secret ) ) {
138 require_once dirname( __FILE__ ) . '/2fa/rfc6238.php';
139 $secret = TokenAuth6238::generateRandomClue();
140 }
141
142 $enc = $this->encrypt( $secret );
143 update_user_option( $user->ID, 'webarx_2fa_secretkey', $enc['cipher'], true );
144 update_user_option( $user->ID, 'webarx_2fa_secretkey_nonce', $enc['nonce'], true );
145 } else {
146 $nonce = get_user_option( 'webarx_2fa_secretkey_nonce', $user->ID );
147 $secret = $this->decrypt( $secret, $nonce );
148 }
149
150 return $secret;
151 }
152
153 /**
154 * Check if the IP address is banned from attempting to guess passwords.
155 *
156 * @return void
157 */
158 public function check_ipban() {
159 if ( is_user_logged_in() || ! $this->get_option( 'patchstack_block_bruteforce_ips', 0 ) ) {
160 return;
161 }
162
163 // Check if the users IP address is whitelisted.
164 $ip = $this->get_ip();
165 if ( $this->plugin->ban->is_ip_whitelisted( $ip ) ) {
166 return;
167 }
168
169 // Calculate block time.
170 $minutes = (int) $this->get_option( 'patchstack_anti_bruteforce_minutes', 30 );
171 $timeout = (int) $this->get_option( 'patchstack_anti_bruteforce_blocktime', 60 );
172 if ( empty( $minutes ) || empty( $timeout ) ) {
173 $time = 30 + 60;
174 } else {
175 $time = $minutes + $timeout;
176 }
177
178 // Check if X failed login attempts were made.
179 global $wpdb;
180 $results = $wpdb->get_results(
181 $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 ] ),
182 OBJECT
183 );
184
185 // Determine the number of attempts.
186 if ( ! isset( $results, $results[0], $results[0]->numIps ) ) {
187 $num = 0;
188 } else {
189 $num = $results[0]->numIps;
190 }
191
192 // Block the user?
193 if ( $num >= $this->get_option( 'patchstack_anti_bruteforce_attempts', 10 ) ) {
194 $this->plugin->firewall_base->display_error_page( 24 );
195 }
196 }
197
198 /**
199 * If logon hours are set, check the current time and allow or disallow the user
200 * to login depending on the settings.
201 *
202 * @return void
203 */
204 public function check_logonhours() {
205 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' ) ) {
206 return;
207 }
208 $block = true;
209
210 // Current time.
211 $hour = current_time( 'G' );
212 $min = current_time( 'i' );
213 $stamp_current = current_time( 'U' );
214
215 // Get time start.
216 $start = explode( ':', str_replace( '00', '0', $this->get_option( 'patchstack_login_time_start', '00:00' ) ) );
217 if ( count( $start ) != 2 ) {
218 return;
219 }
220 $stamp_start = strtotime( current_time( 'Y-m-d' ) . ' ' . $this->get_option( 'patchstack_login_time_start', '00:00' ) . ':00' );
221 $start[0] = (int) $start[0];
222 $start[1] = (int) $start[1];
223
224 // Get time end.
225 $end = explode( ':', str_replace( '00', '0', $this->get_option( 'patchstack_login_time_end', '23:59' ) ) );
226 if ( count( $end ) != 2 ) {
227 return;
228 }
229 $stamp_end = strtotime( current_time( 'Y-m-d' ) . ' ' . $this->get_option( 'patchstack_login_time_end', '00:00' ) . ':00' );
230 $end[0] = (int) $end[0];
231 $end[1] = (int) $end[1];
232
233 // If begin time is earlier than end time.
234 if ( $start[0] <= $end[0] && $stamp_current >= $stamp_start && $stamp_current <= $stamp_end ) {
235 $block = false;
236 }
237
238 // If begin time is later than end time.
239 if ( $start[0] > $end[0] && ( $hour >= $start[0] || $hour <= $end[0] ) ) {
240 $block = false;
241
242 if ( ( $hour == $start[0] && $min < $start[1] ) || ( $hour == $end[0] && $min > $end[1] ) ) {
243 $block = true;
244 }
245 }
246
247 // Block the user?
248 if ( $block ) {
249 wp_die( esc_attr__( 'Access to the login page has been restricted due to set logon hours.', 'patchstack' ), esc_attr__( 'Login Disallowed', 'patchstack' ) );
250 }
251 }
252
253 /**
254 * Determine if we should inject reCAPTCHA into certain pages.
255 *
256 * @return void
257 */
258 public function add_captcha() {
259 switch ( $this->get_option( 'patchstack_captcha_type' ) ) {
260 case 'v2':
261 $public = $this->get_option( 'patchstack_captcha_public_key', '' );
262 $private = $this->get_option( 'patchstack_captcha_private_key', '' );
263 break;
264 case 'invisible':
265 $public = $this->get_option( 'patchstack_captcha_public_key_v3', '' );
266 $private = $this->get_option( 'patchstack_captcha_private_key_v3', '' );
267 break;
268 case 'v3':
269 $public = $this->get_option( 'patchstack_captcha_public_key_v3_new', '' );
270 $private = $this->get_option( 'patchstack_captcha_private_key_v3_new', '' );
271 break;
272 case 'turnstile':
273 $public = $this->get_option( 'patchstack_captcha_public_key_turnstile', '' );
274 $private = $this->get_option( 'patchstack_captcha_private_key_turnstile', '' );
275 break;
276 default:
277 return;
278 break;
279 }
280
281 // Make sure that the keys are set.
282 if ( $public == '' || $private == '' ) {
283 return;
284 }
285
286 // reCAPTCHA on the login page.
287 if ( $this->get_option( 'patchstack_captcha_login_form' ) ) {
288 add_filter( 'login_form', [ $this->plugin->hardening, 'captcha_display' ] );
289 add_filter( 'wp_authenticate_user', [ $this, 'login_captcha_check' ], 10, 2 );
290 }
291
292 // reCAPTCHA on the registration form.
293 if ( $this->get_option( 'patchstack_captcha_registration_form' ) ) {
294 add_action( 'register_form', [ $this->plugin->hardening, 'captcha_display' ] );
295 add_action( 'registration_errors', [ $this, 'general_captcha_check' ] );
296 }
297
298 // reCAPTCHA on the reset password form.
299 if ( $this->get_option( 'patchstack_captcha_reset_pwd_form' ) ) {
300 add_action( 'lostpassword_form', [ $this->plugin->hardening, 'captcha_display' ] );
301 add_action( 'allow_password_reset', [ $this, 'general_captcha_check' ] );
302 }
303 }
304
305 /**
306 * Check reCAPTCHA upon login.
307 *
308 * @param string $user
309 * @param string $password
310 * @return WP_User|WP_Error
311 */
312 public function login_captcha_check( $user, $password ) {
313 $result = $this->plugin->hardening->captcha_check();
314
315 if ( ! $result['response'] ) {
316 if ( $result['reason'] === 'ERROR_NO_KEYS' ) {
317 return $user;
318 }
319 $error_message = sprintf( '<strong>%s</strong>: %s', 'Error', esc_attr__( 'You have entered an incorrect reCAPTCHA value.', 'patchstack' ) );
320
321 if ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) {
322 wp_clear_auth_cookie();
323 return new WP_Error( 'patchstack_error', $error_message );
324 }
325
326 if ( isset( $_REQUEST['log'], $_REQUEST['pwd'] ) ) {
327 return new WP_Error( 'patchstack_error', $error_message );
328 }
329 } else {
330 return $user;
331 }
332 }
333
334 /**
335 * Captcha check for the register or lost password form.
336 *
337 * @param mixed|WP_Error $error
338 * @return WP_Error
339 */
340 public function general_captcha_check( $error ) {
341 $result = $this->plugin->hardening->captcha_check();
342
343 if ( $result['response'] || $result['reason'] == 'ERROR_NO_KEYS' ) {
344 return $error;
345 }
346
347 if ( ! is_wp_error( $error ) ) {
348 $error = new WP_Error();
349 }
350
351 $error->add( 'patchstack_error', 'ERROR' . ':&nbsp;' . esc_attr__( 'You have entered an incorrect reCAPTCHA value. Refresh this page and try again.', 'patchstack' ) );
352 return $error;
353 }
354 }
355