PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | includes/class-two-factor-email.php +38 -66 2.11.122.11.8 View file →
@@ -76,15 +76,12 @@
76 76 $this->database = $database;
77 77 $this->activity_log = $activity_log;
78 78 $this->login_security = $login_security;
79 79
80 - // Resolved on demand, not here: see the note in the TOTP constructor.
81 - $this->options = null;
80 + $login_options = $settings->get_section( 'login_security' );
81 + $this->options = $login_options['two_factor'] ?? array();
82 82
83 - // Same reasoning as in the TOTP class: on a network both classes register
84 - // wherever the login lands, and which one handles a given login is decided
85 - // per account inside check_2fa_requirement().
86 - if ( is_multisite() || ! empty( $this->policy()['enabled'] ) ) {
83 + if ( $this->is_enabled() ) {
87 84 $this->init_hooks();
88 85 }
89 86 }
90 87
@@ -93,13 +90,13 @@
93 90 *
94 91 * @return bool
95 92 */
96 93 public function is_enabled() {
97 - if ( empty( $this->policy()['enabled'] ) ) {
94 + if ( empty( $this->options['enabled'] ) ) {
98 95 return false;
99 96 }
100 97 // Only active when method is email (or not set, for backward compatibility)
101 - $method = $this->policy()['method'] ?? 'email';
98 + $method = $this->options['method'] ?? 'email';
102 99 return 'email' === $method;
103 100 }
104 101
105 102 /**
@@ -186,14 +183,8 @@
186 183 if ( ! $this->user_requires_2fa( $user ) ) {
187 184 return $user;
188 185 }
189 186
190 - // And whether this class is the one that must ask. Both are registered on
191 - // a network; the election is per account (see two_factor_handler_for()).
192 - if ( ! $this->handles_second_factor( $user, 'email' ) ) {
193 - return $user;
194 - }
195 -
196 187 // Check if device is trusted
197 188 if ( $this->is_device_trusted( $user->ID ) ) {
198 189 return $user;
199 190 }
@@ -249,11 +240,24 @@
249 240 * @param WP_User $user User object.
250 241 * @return bool
251 242 */
252 243 public function user_requires_2fa( $user ) {
253 - // One answer for the whole network, same as the TOTP class. See
254 - // Vigilante_Settings::two_factor_required_for().
255 - return Vigilante_Settings::two_factor_required_for( $user );
244 + // Check if user is explicitly excluded
245 + $excluded_users = $this->options['excluded_users'] ?? array();
246 + if ( in_array( $user->ID, array_map( 'absint', $excluded_users ), true ) ) {
247 + return false;
248 + }
249 +
250 + // Check if user has an enforced role
251 + $enforced_roles = $this->options['enforced_roles'] ?? array( 'administrator', 'editor' );
252 +
253 + foreach ( $user->roles as $role ) {
254 + if ( in_array( $role, $enforced_roles, true ) ) {
255 + return true;
256 + }
257 + }
258 +
259 + return false;
256 260 }
257 261
258 262 /**
259 263 * Generate verification code
@@ -265,9 +269,9 @@
265 269 // Generate secure 6-digit code
266 270 $code = sprintf( '%06d', wp_rand( 0, 999999 ) );
267 271
268 272 // Calculate expiry
269 - $expiry_minutes = absint( $this->policy()['code_expiry_minutes'] ?? 10 );
273 + $expiry_minutes = absint( $this->options['code_expiry_minutes'] ?? 10 );
270 274 $expires_at = gmdate( 'Y-m-d H:i:s', time() + ( $expiry_minutes * 60 ) );
271 275
272 276 // Only the hash is stored. The code itself travels in the email and
273 277 // nowhere else, and verify_code() compares with hash_equals() (S11).
@@ -284,15 +288,15 @@
284 288 * @return bool
285 289 */
286 290 private function send_verification_email( $user, $code ) {
287 291 $site_name = get_bloginfo( 'name' );
288 - $from_name = $this->policy()['email_from_name'] ?? '';
292 + $from_name = $this->options['email_from_name'] ?? '';
289 293
290 294 if ( empty( $from_name ) ) {
291 295 $from_name = $site_name;
292 296 }
293 297
294 - $expiry_minutes = absint( $this->policy()['code_expiry_minutes'] ?? 10 );
298 + $expiry_minutes = absint( $this->options['code_expiry_minutes'] ?? 10 );
295 299
296 300 $subject = sprintf(
297 301 /* translators: 1: Site name, 2: Verification code */
298 302 __( '[%1$s] Your verification code: %2$s', 'vigilante' ),
@@ -320,18 +324,8 @@
320 324 /**
321 325 * Handle 2FA verification form submission
322 326 */
323 327 public function handle_2fa_form() {
324 - /*
325 - * Y solo ella la verifica. Volver aqui no deja pasar nada: la otra clase
326 - * esta enganchada a la misma accion y termina la peticion por su cuenta,
327 - * que es lo que evita el fallthrough a wp_signon() que avisa el comentario
328 - * de abajo.
329 - */
330 - if ( ! $this->pending_belongs_to( 'email' ) ) {
331 - return;
332 - }
333 -
334 328 // The pending user is resolved first so that a failed nonce can be
335 329 // explained on the form and recorded (S15). Both failure paths end the
336 330 // request: a bare return would let wp-login.php fall through to its
337 331 // default case and call wp_signon(), completing the login without the
@@ -349,24 +343,12 @@
349 343
350 344 $code = isset( $_POST['vigilante_2fa_code'] ) ? sanitize_text_field( wp_unslash( $_POST['vigilante_2fa_code'] ) ) : '';
351 345 $remember_device = ! empty( $_POST['vigilante_2fa_remember'] );
352 346
353 - // Read before verifying: running out of attempts clears the pending
354 - // session inside verify_code(), and the redirect_to of the original
355 - // login lives there.
356 - $redirect_to = $this->pending_login_redirect();
357 -
358 347 // Verify code
359 348 $result = $this->verify_code( $user_id, $code );
360 349
361 350 if ( is_wp_error( $result ) ) {
362 - // Out of attempts: the session is gone, so the form that would show
363 - // this message is not painted any more. Say it on the login screen
364 - // instead of bouncing the visitor there with no explanation.
365 - if ( 'max_attempts' === $result->get_error_code() ) {
366 - $this->redirect_to_login_with_notice( 'attempts' );
367 - }
368 -
369 351 // Store error for display
370 352 set_transient( 'vigilante_2fa_error_' . $user_id, $result->get_error_message(), 60 );
371 353
372 354 // Redirect back to login
@@ -392,11 +374,11 @@
392 374 wp_set_auth_cookie( $user_id, false );
393 375 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- wp_login is a WordPress core hook that must be fired on login.
394 376 do_action( 'wp_login', $user->user_login, $user );
395 377
396 - // Where the login was headed, or the dashboard. wp_validate_redirect()
397 - // has already dropped anything off this site (pending_login_redirect()).
398 - wp_safe_redirect( $redirect_to );
378 + // Redirect to admin dashboard (always use admin_url to avoid issues with popups,
379 + // malformed URLs, or query parameters that could cause problems)
380 + wp_safe_redirect( admin_url() );
399 381 exit;
400 382 }
401 383
402 384 /**
@@ -406,13 +388,8 @@
406 388 * @param string $code Submitted code.
407 389 * @return true|WP_Error
408 390 */
409 391 private function verify_code( $user_id, $code ) {
410 - // Mail clients and password managers show the code in groups and paste
411 - // it with the separator. Until 2.11.12 that reached wp_hash() as typed
412 - // and every correct code pasted that way came back "invalid".
413 - $code = preg_replace( '/\D/', '', (string) $code );
414 -
415 392 $stored = $this->database->get_2fa_code( $user_id );
416 393 $user = get_user_by( 'ID', $user_id );
417 394
418 395 if ( ! $stored ) {
@@ -430,9 +407,9 @@
430 407 return new WP_Error( 'code_used', __( 'Verification code has already been used. Please log in again.', 'vigilante' ) );
431 408 }
432 409
433 410 // Check max attempts for this specific code
434 - $max_code_attempts = absint( $this->policy()['max_attempts'] ?? 3 );
411 + $max_code_attempts = absint( $this->options['max_attempts'] ?? 3 );
435 412
436 413 if ( absint( $stored['attempts'] ) >= $max_code_attempts ) {
437 414 $this->log_event( '2fa_max_attempts_exceeded', $user_id, __( 'Maximum verification attempts exceeded', 'vigilante' ), 'warning' );
438 415 $this->database->delete_2fa_code( $user_id );
@@ -491,13 +468,8 @@
491 468 /**
492 469 * Maybe show 2FA verification form on login page
493 470 */
494 471 public function maybe_show_2fa_form() {
495 - // Solo la clase que atiende esta verificacion pinta su formulario.
496 - if ( ! $this->pending_belongs_to( 'email' ) ) {
497 - return;
498 - }
499 -
500 472 // Only the visitor presenting the pending token gets the form. There is
501 473 // no fallback by IP address and no lookup of the token by user (S3).
502 474 $session = $this->get_pending_session();
503 475
@@ -511,10 +483,10 @@
511 483 // Get any error message
512 484 $error = get_transient( 'vigilante_2fa_error_' . $user_id );
513 485 delete_transient( 'vigilante_2fa_error_' . $user_id );
514 486
515 - $expiry_minutes = absint( $this->policy()['code_expiry_minutes'] ?? 10 );
516 - $remember_days = absint( $this->policy()['remember_device_days'] ?? 30 );
487 + $expiry_minutes = absint( $this->options['code_expiry_minutes'] ?? 10 );
488 + $remember_days = absint( $this->options['remember_device_days'] ?? 30 );
517 489
518 490 // Hide the normal login form and disable required fields
519 491 ?>
520 492 <style>
@@ -581,10 +553,10 @@
581 553 name="vigilante_2fa_code"
582 554 id="vigilante_2fa_code"
583 555 class="input"
584 556 size="6"
585 - maxlength="20"
586 - pattern="[0-9 -]{6,20}"
557 + maxlength="6"
558 + pattern="[0-9]{6}"
587 559 inputmode="numeric"
588 560 autocomplete="one-time-code"
589 561 autofocus
590 562 required>
@@ -589,9 +561,9 @@
589 561 autofocus
590 562 required>
591 563 </p>
592 564
593 - <?php if ( ! empty( $this->policy()['allow_remember_device'] ) ) : ?>
565 + <?php if ( ! empty( $this->options['allow_remember_device'] ) ) : ?>
594 566 <p class="vigilante-2fa-field vigilante-2fa-remember">
595 567 <label>
596 568 <input type="checkbox" name="vigilante_2fa_remember" value="1">
597 569 <?php
@@ -719,10 +691,10 @@
719 691 * @param bool $only_new Only send to users not previously notified.
720 692 * @return array Result with count of sent emails
721 693 */
722 694 public function send_activation_notifications( $only_new = false ) {
723 - $enforced_roles = $this->policy()['enforced_roles'] ?? array( 'administrator', 'editor' );
724 - $excluded_users = $this->policy()['excluded_users'] ?? array();
695 + $enforced_roles = $this->options['enforced_roles'] ?? array( 'administrator', 'editor' );
696 + $excluded_users = $this->options['excluded_users'] ?? array();
725 697 $excluded_users = array_map( 'absint', $excluded_users );
726 698
727 699 // Get users with enforced roles
728 700 $users = get_users( array(
@@ -738,9 +710,9 @@
738 710 );
739 711 }
740 712
741 713 $site_name = get_bloginfo( 'name' );
742 - $from_name = $this->policy()['email_from_name'] ?? '';
714 + $from_name = $this->options['email_from_name'] ?? '';
743 715 $admin_email = get_option( 'admin_email' );
744 716
745 717 if ( empty( $from_name ) ) {
746 718 $from_name = $site_name;
@@ -745,9 +717,9 @@
745 717 if ( empty( $from_name ) ) {
746 718 $from_name = $site_name;
747 719 }
748 720
749 - $remember_days = absint( $this->policy()['remember_device_days'] ?? 30 );
721 + $remember_days = absint( $this->options['remember_device_days'] ?? 30 );
750 722
751 723 $subject = sprintf(
752 724 /* translators: %s: Site name */
753 725 __( '[%s] Two-factor authentication enabled for your account', 'vigilante' ),
@@ -761,9 +733,9 @@
761 733 $site_name
762 734 )
763 735 );
764 736 $body .= Vigilante_Email_Template::info_box(
765 - ! empty( $this->policy()['allow_remember_device'] )
737 + ! empty( $this->options['allow_remember_device'] )
766 738 ? sprintf(
767 739 /* translators: %d: Remember days */
768 740 __( 'After entering your password, you will receive a 6-digit code via email. You can check "Remember this device" to skip verification for %d days.', 'vigilante' ),
769 741 $remember_days