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

351 lines 11.3 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 || $this->is_community() ) {
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' ) {
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', __( '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', __( '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( __( 'Access to the login page has been restricted due to set logon hours.', 'patchstack' ), __( '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 default:
273 return;
274 break;
275 }
276
277 // Make sure that the keys are set.
278 if ( $public == '' || $private == '' ) {
279 return;
280 }
281
282 // reCAPTCHA on the login page.
283 if ( $this->get_option( 'patchstack_captcha_login_form' ) ) {
284 add_filter( 'login_form', [ $this->plugin->hardening, 'captcha_display' ] );
285 add_filter( 'wp_authenticate_user', [ $this, 'login_captcha_check' ], 10, 2 );
286 }
287
288 // reCAPTCHA on the registration form.
289 if ( $this->get_option( 'patchstack_captcha_registration_form' ) ) {
290 add_action( 'register_form', [ $this->plugin->hardening, 'captcha_display' ] );
291 add_action( 'registration_errors', [ $this, 'general_captcha_check' ] );
292 }
293
294 // reCAPTCHA on the reset password form.
295 if ( $this->get_option( 'patchstack_captcha_reset_pwd_form' ) ) {
296 add_action( 'lostpassword_form', [ $this->plugin->hardening, 'captcha_display' ] );
297 add_action( 'allow_password_reset', [ $this, 'general_captcha_check' ] );
298 }
299 }
300
301 /**
302 * Check reCAPTCHA upon login.
303 *
304 * @param string $user
305 * @param string $password
306 * @return WP_User|WP_Error
307 */
308 public function login_captcha_check( $user, $password ) {
309 $result = $this->plugin->hardening->captcha_check();
310
311 if ( ! $result['response'] ) {
312 if ( $result['reason'] === 'ERROR_NO_KEYS' ) {
313 return $user;
314 }
315 $error_message = sprintf( '<strong>%s</strong>: %s', 'Error', __( 'You have entered an incorrect reCAPTCHA value.', 'patchstack' ) );
316
317 if ( $result['reason'] === 'VERIFICATION_FAILED' || $result['reason'] === 'RECAPTCHA_EMPTY_RESPONSE' ) {
318 wp_clear_auth_cookie();
319 return new WP_Error( 'patchstack_error', $error_message );
320 }
321
322 if ( isset( $_REQUEST['log'], $_REQUEST['pwd'] ) ) {
323 return new WP_Error( 'patchstack_error', $error_message );
324 }
325 } else {
326 return $user;
327 }
328 }
329
330 /**
331 * Captcha check for the register or lost password form.
332 *
333 * @param mixed|WP_Error $error
334 * @return WP_Error
335 */
336 public function general_captcha_check( $error ) {
337 $result = $this->plugin->hardening->captcha_check();
338
339 if ( $result['response'] || $result['reason'] == 'ERROR_NO_KEYS' ) {
340 return $error;
341 }
342
343 if ( ! is_wp_error( $error ) ) {
344 $error = new WP_Error();
345 }
346
347 $error->add( 'patchstack_error', 'ERROR' . ':&nbsp;' . __( 'You have entered an incorrect reCAPTCHA value. Refresh this page and try again.', 'patchstack' ) );
348 return $error;
349 }
350 }
351