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-login-security.php +10 -176 2.11.122.11.8 View file →
@@ -101,9 +101,9 @@
101 101 // Check lockout before authentication
102 102 add_filter( 'authenticate', array( $this, 'check_lockout' ), 30, 3 );
103 103
104 104 // Track login attempts
105 - add_action( 'wp_login_failed', array( $this, 'handle_failed_login' ), 10, 2 );
105 + add_action( 'wp_login_failed', array( $this, 'handle_failed_login' ) );
106 106 add_action( 'wp_login', array( $this, 'handle_successful_login' ), 10, 2 );
107 107
108 108 // Hide login errors
109 109 if ( ! empty( $this->options['hide_login_errors'] ) ) {
@@ -472,49 +472,14 @@
472 472 if ( defined( 'VIGILANTE_CUSTOM_LOGIN' ) && VIGILANTE_CUSTOM_LOGIN ) {
473 473 return;
474 474 }
475 475
476 - /*
477 - * Everything from here down reaches wp-login.php WITHOUT having gone
478 - * through the secret address, so nothing rendered from here may carry
479 - * it. The filters this class registers rewrite the form action and
480 - * every login link to the slug, and wp-login.php prints them on each
481 - * page it serves, so any request let through below handed the hidden
482 - * address to whoever asked for it.
483 - *
484 - * Measured on 12 sep 2026 with the slug configured: a plain anonymous
485 - * GET of ?action=lostpassword or ?action=retrievepassword returned it
486 - * three times, ?password=changed twice and ?checkemail=confirm once.
487 - * The exemptions those requests use (the allowed actions and the
488 - * informational query strings) have been there since hiding the login
489 - * existed, so the address was never actually hidden from anyone who
490 - * asked for a password reset page.
491 - *
492 - * The first attempt at this fix dropped the filters only for POST, and
493 - * only helped the POST with no action: the cross review of 2.11.10
494 - * found the four GETs and the POST to ?action=lostpassword, which still
495 - * leaked through lostpassword_redirect. The rule is now one rule, not a
496 - * list of shapes: came in by the slug, or the address is not emitted.
497 - *
498 - * A visitor with a session is the single exception, and on purpose:
499 - * they already have access, and logging out has to land on the hidden
500 - * address or core's redirect to ?loggedout=true would 404.
501 - */
502 - if ( ! is_user_logged_in() ) {
503 - $this->stop_emitting_custom_login_url();
504 - }
505 -
506 - /*
507 - * A POST is let through so a remote manager such as MainWP or ManageWP
508 - * can authenticate, which is what this exemption has always existed for.
509 - * Authentication is unaffected by the lines above, because a successful
510 - * login redirects to the destination and never renders this form.
511 - */
476 + // Allow POST requests (form submissions)
512 477 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
513 478 if ( 'POST' === $request_method ) {
514 479 return;
515 480 }
516 -
481 +
517 482 // Allow AJAX requests
518 483 if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
519 484 return;
520 485 }
@@ -536,30 +501,8 @@
536 501 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
537 502 if ( isset( $_GET['checkemail'] ) || isset( $_GET['password'] ) ) {
538 503 return;
539 504 }
540 -
541 - /*
542 - * A visitor in the middle of a second factor verification. Every redirect
543 - * of that flow goes to wp_login_url(), and with the address filters
544 - * dropped that is the plain wp-login.php, so a wrong code, an expired
545 - * nonce or running out of attempts landed on a 404 with no way back: a
546 - * login started by POST straight at wp-login.php could be begun but never
547 - * finished. Found by the third cross review of 2.11.10, which measured
548 - * 2.11.9 returning the visitor to the login form and trunk returning 404.
549 - *
550 - * The proof asked for is the pending session itself, not the cookie:
551 - * a made-up token finds no transient and gets the 404 like anybody else,
552 - * and the real one is only issued after the right password. Same key as
553 - * the trait (trait-two-factor-session.php:91 and :201).
554 - */
555 - if ( isset( $_COOKIE['vigilante_2fa_token'] ) ) {
556 - $pending = get_transient( 'vigilante_2fa_pending_' . sanitize_text_field( wp_unslash( $_COOKIE['vigilante_2fa_token'] ) ) );
557 -
558 - if ( is_array( $pending ) && ! empty( $pending['user_id'] ) ) {
559 - return;
560 - }
561 - }
562 505
563 506 // Check if user already logged in - redirect to admin
564 507 if ( is_user_logged_in() ) {
565 508 wp_safe_redirect( admin_url() );
@@ -721,53 +664,12 @@
721 664 * @param string $register_url The register URL.
722 665 * @return string
723 666 */
724 667 public function filter_register_url( $register_url ) {
725 - /*
726 - * Not from wp-signup.php. On a single site the core answers that file
727 - * with wp_redirect( wp_registration_url() ) and dies (wp-signup.php:39-41),
728 - * so this filter put the secret address in the Location header of a plain
729 - * anonymous request, outside wp-login.php and therefore out of reach of
730 - * block_wp_login_access(), which only runs on login_init. Measured by the
731 - * third cross review of 2.11.10; present since hiding the login existed.
732 - *
733 - * Left unfiltered, that redirect lands on wp-login.php?action=register,
734 - * which the blocker answers with the same 404 as any other direct visit,
735 - * which is what hiding the login is for. The registration link served on
736 - * the login page itself is rendered under the slug, where this filter goes
737 - * on doing its job.
738 - */
739 - if ( $this->request_is_signup() ) {
740 - return $register_url;
741 - }
742 -
743 668 return add_query_arg( 'action', 'register', home_url( $this->custom_login_slug . '/' ) );
744 669 }
745 670
746 671 /**
747 - * Whether this request is being served by wp-signup.php or wp-activate.php
748 - *
749 - * @since 2.11.10
750 - *
751 - * @return bool
752 - */
753 - private function request_is_signup() {
754 - foreach ( array( 'SCRIPT_NAME', 'PHP_SELF', 'SCRIPT_FILENAME' ) as $key ) {
755 - if ( empty( $_SERVER[ $key ] ) ) {
756 - continue;
757 - }
758 -
759 - $file = basename( sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) );
760 -
761 - if ( 'wp-signup.php' === $file || 'wp-activate.php' === $file ) {
762 - return true;
763 - }
764 - }
765 -
766 - return false;
767 - }
768 -
769 - /**
770 672 * Redirect after a successful lost-password request to the custom login URL
771 673 *
772 674 * Without this filter, core sends the user to wp-login.php?checkemail=confirm,
773 675 * which 404s when the custom login URL is enabled (block_wp_login_access only
@@ -964,10 +866,11 @@
964 866 // This rejection is ours, not a wrong password. wp_authenticate()
965 867 // still fires wp_login_failed for it, and until 2.11.0 that counted
966 868 // the blocked attempt as one more failure, which rewrote the row's
967 869 // status and produced a fresh lockout, with its critical entry and
968 - // its email, on every POST made during the lockout (S8). The error
969 - // code below is what handle_failed_login() reads to leave it alone.
870 + // its email, on every POST made during the lockout (S8).
871 + add_filter( 'vigilante_skip_failed_login_count', '__return_true' );
872 +
970 873 return new WP_Error(
971 874 'vigilante_lockout',
972 875 sprintf(
973 876 /* translators: %d: Minutes remaining */
@@ -980,56 +883,15 @@
980 883 return $user;
981 884 }
982 885
983 886 /**
984 - * Error codes of the refusals Vigilant issues itself
985 - *
986 - * WordPress treats every WP_Error out of the authenticate chain as a failed
987 - * login and fires wp_login_failed for it (wp-includes/pluggable.php,
988 - * wp_authenticate()). These seven are not wrong passwords: the credentials
989 - * were right and Vigilant stopped the login for a reason of its own, so none
990 - * of them counts towards the brute force lockout.
991 - *
992 - * The rejection identifies itself by the error code it carries. Until
993 - * 2.11.12 each one instead added a filter that stayed registered for the
994 - * rest of the request, which meant that one controlled rejection stopped
995 - * every later failed login of the same request from being counted: measured
996 - * on 17 Sep 2026, three wrong passwords for a different account, sent in the
997 - * same request, none of them recorded. A single XML-RPC system.multicall is
998 - * enough to make that one request.
999 - *
1000 - * @since 2.11.12
1001 - *
1002 - * @var string[]
1003 - */
1004 - const CONTROLLED_REJECTIONS = array(
1005 - 'vigilante_2fa_required', // Two factor asked for, by app or by email.
1006 - 'vigilante_lockout', // This address is already locked out.
1007 - 'vigilante_force_reset', // An administrator forced a password reset.
1008 - 'vigilante_password_expired', // Password expiry policy, over XML-RPC.
1009 - 'pending_approval', // Registration awaiting approval.
1010 - 'session_limit_exceeded', // Too many sessions already open.
1011 - 'email_not_verified', // Email address not verified yet.
1012 - );
1013 -
1014 - /**
1015 887 * Handle failed login attempt
1016 888 *
1017 - * @since 2.11.12 Receives the WP_Error, so a refusal of Vigilant's own is told
1018 - * apart from a wrong password by what it is and not by a flag
1019 - * left behind for the rest of the request.
1020 - *
1021 - * @param string $username Username that failed.
1022 - * @param WP_Error|null $error The error WordPress is reporting, if any.
889 + * @param string $username Username that failed.
1023 890 */
1024 - public function handle_failed_login( $username, $error = null ) {
1025 - // A refusal of ours, not a wrong password.
1026 - if ( $error instanceof WP_Error && array_intersect( $error->get_error_codes(), self::CONTROLLED_REJECTIONS ) ) {
1027 - return;
1028 - }
1029 -
1030 - // Kept for anything outside the plugin that marks its own controlled
1031 - // rejection. Nothing inside Vigilant sets it any more.
891 + public function handle_failed_login( $username ) {
892 + // Skip counting if this is a Vigilante-controlled rejection
893 + // (pending approval, session limit, email verification, etc.)
1032 894 if ( apply_filters( 'vigilante_skip_failed_login_count', false ) ) {
1033 895 return;
1034 896 }
1035 897
@@ -1644,36 +1506,8 @@
1644 1506 * blockers already let every POST through, which is how they authenticate.
1645 1507 *
1646 1508 * @return bool
1647 1509 */
1648 - /**
1649 - * Stop this request from putting the secret login address in any URL
1650 - *
1651 - * The filters that rewrite a WordPress login URL into the custom slug are
1652 - * what makes the feature work, and also what leaks it the moment a page is
1653 - * rendered on a path the visitor was not supposed to reach. Dropping them
1654 - * for the rest of the request keeps whatever renders afterwards free of the
1655 - * slug, while everything else about the request goes on as before.
1656 - *
1657 - * The two redirect filters are here because the first version of this list
1658 - * had only the five URL ones, and a POST to ?action=lostpassword still
1659 - * handed the address over in the hidden redirect_to field that
1660 - * lostpassword_redirect fills. Found by the cross review of 2.11.10. Any
1661 - * filter of this class that can put the slug in front of a visitor belongs
1662 - * in this list; if a new one is added, add it here too.
1663 - *
1664 - * @since 2.11.10
1665 - */
1666 - private function stop_emitting_custom_login_url() {
1667 - remove_filter( 'site_url', array( $this, 'filter_site_url' ), 10 );
1668 - remove_filter( 'login_url', array( $this, 'filter_login_url' ), 10 );
1669 - remove_filter( 'logout_url', array( $this, 'filter_logout_url' ), 10 );
1670 - remove_filter( 'lostpassword_url', array( $this, 'filter_lostpassword_url' ), 10 );
1671 - remove_filter( 'register_url', array( $this, 'filter_register_url' ), 10 );
1672 - remove_filter( 'lostpassword_redirect', array( $this, 'filter_lostpassword_redirect' ), 10 );
1673 - remove_filter( 'logout_redirect', array( $this, 'filter_logout_redirect' ), 10 );
1674 - }
1675 -
1676 1510 private function is_ip_exempt_from_hiding() {
1677 1511 $whitelist = $this->settings->get_option( 'firewall', 'ip_whitelist', array() );
1678 1512
1679 1513 if ( empty( $whitelist ) ) {