user_login : $username; if ( NLSSL_Core::setting( 'block_admin_username', 0 ) && 'admin' === strtolower( trim( $account_name ) ) ) { NLSSL_Core::log( 'blocked_admin_username', 'warning', 'A login using the blocked admin username was rejected.' ); return new WP_Error( 'nlssl_admin_username_blocked', __( 'Login failed. Check your credentials and try again.', 'neverlast-security-lite' ) ); } if ( ! NLSSL_Core::setting( 'login_rate_limit_enabled', 1 ) || '' === $username ) { return $user; } $user_bucket = 'login_lock_user_' . self::account_hash( $username ); $account_locked = NLSSL_Core::rate_limited( $user_bucket, 1, false ); $ip_locked = NLSSL_Core::setting( 'login_ip_rate_limit_enabled', 0 ) && NLSSL_Core::rate_limited( 'login_lock_ip' ); if ( $account_locked || $ip_locked ) { NLSSL_Core::log( 'login_rate_limited', 'critical', 'Login was temporarily blocked after repeated failures.' ); return new WP_Error( 'nlssl_rate_limited', __( 'Too many login attempts. Try again later.', 'neverlast-security-lite' ) ); } return $user; } public static function failed_login( string $username ): void { if ( NLSSL_Core::setting( 'login_rate_limit_enabled', 1 ) && '' !== $username ) { $max = max( 2, (int) NLSSL_Core::setting( 'login_max_attempts', 5 ) ); $window = max( 1, (int) NLSSL_Core::setting( 'login_window_minutes', 15 ) ) * MINUTE_IN_SECONDS; $lockout = max( 1, (int) NLSSL_Core::setting( 'login_lockout_minutes', 30 ) ) * MINUTE_IN_SECONDS; $user_hash = self::account_hash( $username ); $user_bucket = 'login_attempt_user_' . $user_hash; if ( NLSSL_Core::rate_hit( $user_bucket, $window, $max - 1, false ) ) { NLSSL_Core::rate_hit( 'login_lock_user_' . $user_hash, $lockout, 0, false ); } if ( NLSSL_Core::setting( 'login_ip_rate_limit_enabled', 0 ) && NLSSL_Core::rate_hit( 'login_attempt_ip', $window, max( 10, $max * 4 ) - 1 ) ) { NLSSL_Core::rate_hit( 'login_lock_ip', $lockout, 0 ); } } NLSSL_Core::log( 'login_failed', 'warning', 'Failed login attempt.' ); } public static function successful_login( string $login, WP_User $user ): void { $hash = self::account_hash( $login, $user ); NLSSL_Core::rate_clear( 'login_attempt_user_' . $hash, false ); NLSSL_Core::rate_clear( 'login_lock_user_' . $hash, false ); if ( user_can( $user, 'manage_options' ) && NLSSL_Core::setting( 'admin_login_email', 0 ) ) { NLSSL_Core::notification( 'admin-login-' . $user->ID, 'Administrator login', static function() use ( $user ): string { $body = 'An administrator account signed in. Review the activity log if this was unexpected.'; $link = NLSSL_Emergency::login_response_link( (int) $user->ID ); if ( '' !== $link ) { $body .= "\n\nEmergency response (expires in 15 minutes):\n" . $link . "\nOpening the page performs no action. The separate emergency secret is required to freeze logins."; } return $body; } ); } } public static function generic_error( string $error ): string { return NLSSL_Core::setting( 'generic_login_errors', 1 ) ? __( 'Login failed. Check your credentials and try again.', 'neverlast-security-lite' ) : $error; } public static function xmlrpc_enabled( bool $enabled ): bool { return NLSSL_Core::setting( 'disable_xmlrpc', 1 ) ? false : $enabled; } public static function xmlrpc_methods( array $methods ): array { return NLSSL_Core::setting( 'disable_xmlrpc', 1 ) ? array() : $methods; } public static function xmlrpc_headers( array $headers ): array { if ( NLSSL_Core::setting( 'disable_xmlrpc', 1 ) ) { foreach ( array_keys( $headers ) as $name ) { if ( 0 === strcasecmp( (string) $name, 'X-Pingback' ) ) { unset( $headers[ $name ] ); } } } return $headers; } public static function application_passwords_available( bool $available ): bool { return self::is_frozen() || NLSSL_Core::setting( 'disable_application_passwords', 0 ) ? false : $available; } public static function protect_rest_users( array $endpoints ): array { // This setting protects public enumeration. Once WordPress has authenticated a // user, keep the native users/me and role-appropriate user routes available and // let each REST controller enforce its own permission callbacks. if ( ! NLSSL_Core::setting( 'protect_rest_users', 1 ) || is_user_logged_in() ) { return $endpoints; } foreach ( array_keys( $endpoints ) as $route ) { if ( preg_match( '#^/wp/v2/users(?:/|$)#', (string) $route ) ) { unset( $endpoints[ $route ] ); } } return $endpoints; } public static function remove_user_sitemap( $provider, string $name ) { return 'users' === $name && NLSSL_Core::setting( 'block_author_enumeration', 1 ) ? false : $provider; } public static function block_author_enumeration(): void { if ( ! NLSSL_Core::setting( 'block_author_enumeration', 1 ) || is_admin() ) { return; } if ( isset( $_GET['author'] ) || isset( $_GET['author_name'] ) || is_author() ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Presence check on a public URL; the value is never read. NLSSL_Core::log( 'author_enumeration_blocked', 'warning', 'An author-enumeration request was blocked.' ); global $wp_query; if ( $wp_query instanceof WP_Query ) { $wp_query->set_404(); } status_header( 404 ); nocache_headers(); $template = get_404_template(); if ( $template ) { include $template; } else { echo esc_html__( 'Not found.', 'neverlast-security-lite' ); } exit; } } public static function freeze( int $minutes = 60 ): int|WP_Error { if ( defined( 'NLSSL_DISABLE_LOGIN_FREEZE' ) && NLSSL_DISABLE_LOGIN_FREEZE ) { return new WP_Error( 'nlssl_freeze_disabled', __( 'Login Freeze is disabled by the recovery configuration. No freeze was activated.', 'neverlast-security-lite' ) ); } if ( ! NLSSL_Core::acquire_lock( 'nlssl_freeze_action_lock', 30 ) ) { return new WP_Error( 'nlssl_freeze_busy', __( 'The emergency action is busy or storage is unavailable. Retry this link.', 'neverlast-security-lite' ) ); } try { $old = NLSSL_Core::persisted_option( self::FREEZE_OPTION, 0 ); if ( is_wp_error( $old ) ) { return $old; } $until = max( (int) $old, time() + max( 5, min( 1440, $minutes ) ) * MINUTE_IN_SECONDS ); if ( ! NLSSL_Core::write_option_verified( self::FREEZE_OPTION, $until ) || ! self::is_frozen() ) { NLSSL_Core::log( 'emergency_login_freeze_failed', 'critical', 'Login Freeze could not be verified. The action did not complete.' ); return new WP_Error( 'nlssl_freeze_storage_failed', __( 'Login Freeze was not confirmed. The emergency link was retained; repair storage and retry.', 'neverlast-security-lite' ) ); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- An emergency freeze invalidates every pending two-factor challenge in the plugin's own table at once; it must take effect immediately. $challenges = false !== $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', NLSSL_Core::table( '2fa_challenges' ) ) ); $count = self::logout_all_users(); $complete = $challenges && ! is_wp_error( $count ); if ( ! $complete ) { NLSSL_Core::degraded_storage( 'freeze_invalidation' ); } $message = $complete ? 'All WordPress sessions were invalidated and login is frozen.' : 'Login is frozen, but session/challenge invalidation did not complete. Existing sessions may remain active; investigate and retry.'; NLSSL_Core::log( $complete ? 'emergency_login_freeze' : 'emergency_login_freeze_partial', 'critical', $message, array( 'until' => $until, 'challenges_cleared' => $challenges, 'sessions_invalidated' => ! is_wp_error( $count ) ) ); NLSSL_Core::alert( 'login-freeze', $complete ? 'Emergency Login Freeze activated' : 'Emergency Login Freeze partially completed', $message . ' A completed administrator password reset lifts the freeze.' ); return $complete ? $until : new WP_Error( 'nlssl_freeze_partial', __( 'Login is frozen, but existing sessions or login challenges may remain active. Repair storage and retry this emergency link.', 'neverlast-security-lite' ), array( 'freeze_active' => true, 'until' => $until, 'challenges_cleared' => $challenges, 'sessions_invalidated' => ! is_wp_error( $count ) ) ); } finally { NLSSL_Core::release_lock( 'nlssl_freeze_action_lock' ); } } public static function unfreeze(): bool { delete_option( self::FREEZE_OPTION ); if ( null !== NLSSL_Core::persisted_option( self::FREEZE_OPTION, null ) ) { NLSSL_Core::degraded_storage( 'freeze_delete' ); return false; } NLSSL_Core::log( 'emergency_login_unfreeze', 'warning', 'Emergency Login Freeze was lifted.' ); return true; } public static function is_frozen(): bool { if ( defined( 'NLSSL_DISABLE_LOGIN_FREEZE' ) && NLSSL_DISABLE_LOGIN_FREEZE ) { return false; } $until = (int) get_option( self::FREEZE_OPTION, 0 ); if ( $until > 0 && $until <= time() ) { delete_option( self::FREEZE_OPTION ); return false; } return $until > time(); } public static function freeze_message( string $message ): string { if ( ! self::is_frozen() ) { return $message; } return $message . '

' . esc_html__( 'Emergency Login Freeze is active. Use Lost your password? to recover access and lift the freeze.', 'neverlast-security-lite' ) . '

'; } public static function lift_after_password_reset( WP_User $user ): void { // A verified password reset is the advertised recovery path. Clear only this // account's attempt/lock buckets so the new credential can actually be used. $hash = self::account_hash( (string) $user->user_login, $user ); NLSSL_Core::rate_clear( 'login_attempt_user_' . $hash, false ); NLSSL_Core::rate_clear( 'login_lock_user_' . $hash, false ); if ( self::is_frozen() && user_can( $user, 'manage_options' ) ) { if ( self::unfreeze() ) { NLSSL_Core::log( 'login_freeze_recovered', 'warning', 'Emergency Login Freeze was lifted after a verified password reset.', array( 'user_id' => $user->ID ) ); } } } public static function logout_all_users(): int|WP_Error { try { if ( ! class_exists( 'WP_Session_Tokens' ) ) { require_once ABSPATH . WPINC . '/class-wp-session-tokens.php'; } global $wpdb; $deadline = microtime( true ) + 5.0; $count = 0; $offset = 0; do { $ids = get_users( array( 'fields' => 'ID', 'number' => 250, 'offset' => $offset ) ); if ( ! empty( $wpdb->last_error ) ) { return new WP_Error( 'nlssl_sessions_failed', 'Session invalidation could not be verified.' ); } foreach ( $ids as $id ) { if ( microtime( true ) >= $deadline ) { return new WP_Error( 'nlssl_sessions_incomplete', 'Session invalidation reached its time limit.' ); } $manager = WP_Session_Tokens::get_instance( (int) $id ); $manager->destroy_all(); if ( ! empty( $wpdb->last_error ) ) { return new WP_Error( 'nlssl_sessions_failed', 'Session invalidation could not be verified.' ); } $remaining = $manager->get_all(); if ( ! empty( $wpdb->last_error ) || ! empty( $remaining ) ) { return new WP_Error( 'nlssl_sessions_failed', 'Session invalidation could not be verified.' ); } $count++; } $offset += count( $ids ); } while ( count( $ids ) === 250 ); return $count; } catch ( Throwable $error ) { NLSSL_Core::degraded_storage( 'session_provider_exception' ); return new WP_Error( 'nlssl_sessions_failed', 'Session invalidation could not be verified.' ); } } private static function resolve_account( string $login ) { if ( ! function_exists( 'get_user_by' ) || '' === trim( $login ) ) { return false; } $login = trim( $login ); $account = get_user_by( 'login', $login ); return $account instanceof WP_User ? $account : get_user_by( 'email', $login ); } private static function account_hash( string $login, ?WP_User $account = null ): string { $account = $account ?? self::resolve_account( $login ); // Unknown names cannot allocate an unbounded number of durable login counters. $shard = hexdec( substr( hash_hmac( 'sha256', NLSSL_Core::client_ip(), wp_salt( 'nonce' ) ), 0, 2 ) ) % 32; $identity = $account instanceof WP_User ? 'user:' . (int) $account->ID : 'unknown-source:' . $shard; return substr( hash_hmac( 'sha256', $identity, wp_salt( 'nonce' ) ), 0, 24 ); } }