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 +57 -49 2.10.02.11.8 View file →
@@ -862,8 +862,15 @@
862 862 'warning'
863 863 );
864 864 }
865 865
866 + // This rejection is ours, not a wrong password. wp_authenticate()
867 + // still fires wp_login_failed for it, and until 2.11.0 that counted
868 + // the blocked attempt as one more failure, which rewrote the row's
869 + // status and produced a fresh lockout, with its critical entry and
870 + // its email, on every POST made during the lockout (S8).
871 + add_filter( 'vigilante_skip_failed_login_count', '__return_true' );
872 +
866 873 return new WP_Error(
867 874 'vigilante_lockout',
868 875 sprintf(
869 876 /* translators: %d: Minutes remaining */
@@ -933,8 +940,15 @@
933 940 // Get failed attempts in the last hour
934 941 $failed_count = $this->database->get_failed_attempt_count( $ip, 60 );
935 942
936 943 if ( $failed_count >= $max_attempts ) {
944 + // Already locked out: every further POST during the lockout used to
945 + // write another critical entry and send another email (S8). The
946 + // lockout itself is what check_lockout() enforces; nothing to add.
947 + if ( $this->database->is_locked_out( $ip ) ) {
948 + return;
949 + }
950 +
937 951 // Calculate lockout duration with increment
938 952 if ( ! empty( $this->options['lockout_increment'] ) ) {
939 953 $previous_lockouts = $this->get_previous_lockout_count( $ip );
940 954 $lockout_duration = min(
@@ -1116,11 +1130,13 @@
1116 1130 // Login Security
1117 1131 'vigilante_lockout',
1118 1132 // User Security
1119 1133 'vigilante_force_reset',
1120 - 'pending_approval',
1121 - 'email_not_verified',
1122 - 'session_limit_exceeded',
1134 + // pending_approval, email_not_verified and session_limit_exceeded
1135 + // are deliberately NOT here since 2.11.0: they are only raised once
1136 + // the password is correct, so letting them through told an
1137 + // unauthenticated visitor which accounts exist (S10). Those users
1138 + // learn their status from the registration and verification emails.
1123 1139 // Two-Factor Email
1124 1140 'no_code',
1125 1141 'code_expired',
1126 1142 'code_used',
@@ -1178,14 +1194,10 @@
1178 1194 // Note: this fallback won't match on translated sites — the
1179 1195 // code-based check above is the locale-safe path.
1180 1196 $allowed_patterns = array(
1181 1197 'vigilante_lockout',
1182 - 'Account pending',
1183 - 'pending_approval',
1184 - 'email_not_verified',
1185 - 'verify your email',
1186 - 'session_limit',
1187 - 'too many active',
1198 + // The pending-approval, unverified-email and session-limit strings
1199 + // were removed in 2.11.0 for the same reason as their codes above (S10).
1188 1200 'verification code',
1189 1201 'authenticator app',
1190 1202 'two-factor',
1191 1203 'grace period',
@@ -1210,15 +1222,15 @@
1210 1222 * @return array
1211 1223 */
1212 1224 public function remove_shake_errors( $codes ) {
1213 1225 // Keep shake for Vigilante-specific errors that indicate real problems
1214 - // Do NOT include 2FA codes - the form transition should be smooth
1226 + // Do NOT include 2FA codes - the form transition should be smooth.
1227 + // The three account-status codes are not here either since 2.11.0: a
1228 + // shake that only plays for existing accounts is the same tell as the
1229 + // message it replaced (S10).
1215 1230 return array(
1216 1231 'vigilante_lockout',
1217 1232 'vigilante_force_reset',
1218 - 'pending_approval',
1219 - 'email_not_verified',
1220 - 'session_limit_exceeded',
1221 1233 );
1222 1234 }
1223 1235
1224 1236
@@ -1287,9 +1299,9 @@
1287 1299 __( 'Failed attempts', 'vigilante' ) => (string) $attempts,
1288 1300 __( 'Lockout duration', 'vigilante' ) => ceil( $duration / 60 ) . ' ' . __( 'minutes', 'vigilante' ),
1289 1301 __( 'Date/Time', 'vigilante' ) => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ),
1290 1302 ) );
1291 - $body .= Vigilante_Email_Template::button( admin_url( 'admin.php?page=vigilante&tab=login' ), __( 'View lockouts', 'vigilante' ) );
1303 + $body .= Vigilante_Email_Template::button( admin_url( 'admin.php?page=vigilante&tab=login#vigilante-section-login-status' ), __( 'View lockouts', 'vigilante' ) );
1292 1304
1293 1305 Vigilante_Email_Template::send( $to, $subject, __( 'Login lockout triggered', 'vigilante' ), $body, true );
1294 1306 }
1295 1307
@@ -1376,8 +1388,39 @@
1376 1388 if ( ! empty( $whitelist ) && Vigilante_IP_Utils::in_list( Vigilante_IP_Utils::get_client_ip(), $whitelist ) ) {
1377 1389 return;
1378 1390 }
1379 1391
1392 + /*
1393 + * Last, and only for a request that was about to be turned away: whether
1394 + * anybody is actually there.
1395 + *
1396 + * A remote manager signs its own call with a token and asks for the
1397 + * dashboard before holding any cookie; its connector resolves the user
1398 + * through determine_current_user and only then, on 'init', sets the
1399 + * cookie and redirects. Turning the request away here, three hooks
1400 + * earlier, means the connector never reaches the point where it would
1401 + * have logged itself in, so it reads the 404 as a site that is broken
1402 + * and retries the whole job. Observed in the wild with
1403 + * ModularConnector/3.2.1, whose every request landed here.
1404 + *
1405 + * Which is also why this went unnoticed for two releases: a connector
1406 + * that already holds a cookie by the time it asks for the dashboard
1407 + * leaves at has_session_cookie() above and never reaches this line. How
1408 + * many connectors work that way is not something to guess at here; what
1409 + * is certain is that reports only came from sites where one did not.
1410 + *
1411 + * The criterion is the one block_wp_admin_access() has always applied,
1412 + * brought to the door 2.9.9 put in front of it. It costs nothing on the
1413 + * ordinary request, which left long before reaching this line, and
1414 + * nothing on the database either: with no cookie to validate, the three
1415 + * core determine_current_user callbacks all decline without a query. The
1416 + * rejection below already pays for an INSERT into the activity log, and
1417 + * resolves this very same user one step later to record who was refused.
1418 + */
1419 + if ( get_current_user_id() ) {
1420 + return;
1421 + }
1422 +
1380 1423 self::log_early_hidden_admin_attempt();
1381 1424
1382 1425 status_header( 404 );
1383 1426 nocache_headers();
@@ -1506,43 +1549,8 @@
1506 1549 public function get_locked_out_ips() {
1507 1550 return $this->database->get_locked_out_ips();
1508 1551 }
1509 1552
1510 - /**
1511 - * Get login statistics
1512 - *
1513 - * @param int $days Days to look back.
1514 - * @return array
1515 - */
1516 - public function get_statistics( $days = 7 ) {
1517 - global $wpdb;
1518 -
1519 - $table = esc_sql( $this->database->get_login_attempts_table() );
1520 - $since = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
1521 -
1522 - // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1523 - $stats = $wpdb->get_row(
1524 - $wpdb->prepare(
1525 - "SELECT
1526 - COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_attempts,
1527 - COUNT(CASE WHEN status = 'lockout' THEN 1 END) as lockouts,
1528 - COUNT(DISTINCT ip_address) as unique_ips,
1529 - COUNT(DISTINCT username) as unique_usernames
1530 - FROM `{$table}`
1531 - WHERE last_attempt >= %s",
1532 - $since
1533 - ),
1534 - ARRAY_A
1535 - );
1536 - // phpcs:enable
1537 -
1538 - return $stats ? $stats : array(
1539 - 'failed_attempts' => 0,
1540 - 'lockouts' => 0,
1541 - 'unique_ips' => 0,
1542 - 'unique_usernames' => 0,
1543 - );
1544 - }
1545 1553 }
1546 1554
1547 1555 /**
1548 1556 * Disabled XML-RPC Server class