Plugin Directory

Changeset 3422588


Ignore:
Timestamp:
12/18/2025 07:40:50 AM (3 months ago)
Author:
cloudsecure
Message:

2段階認証に関する軽微な修正

Location:
cloudsecure-wp-security/trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • cloudsecure-wp-security/trunk/cloudsecure-wp.php

    r3408912 r3422588  
    1414 * Plugin URI:    https://wpplugin.cloudsecure.ne.jp/cloudsecure_wp_security
    1515 * Description:   管理画面とログインURLをサイバー攻撃から守る、安心の国産・日本語対応プラグインです。かんたんな設定を行うだけで、不正アクセスや不正ログインからあなたのWordPressを保護し、セキュリティが向上します。また、各機能の有効・無効(ON・OFF)や設定などをお好みにカスタマイズし、いつでも保護状態を管理できます。
    16  * Version:       1.3.21
     16 * Version:       1.3.22
    1717 * Requires PHP:  7.1
    1818 * Author:        CloudSecure,Inc.
  • cloudsecure-wp-security/trunk/modules/cloudsecure-wp.php

    r3377803 r3422588  
    6363        $this->captcha                    = new CloudSecureWP_CAPTCHA( $info, $this->config );
    6464        $this->login_log                  = new CloudSecureWP_Login_Log( $info, $this->config, $this->disable_login );
    65         $this->two_factor_authentication  = new CloudSecureWP_Two_Factor_Authentication( $info, $this->config, $this->disable_login );
     65        $this->two_factor_authentication  = new CloudSecureWP_Two_Factor_Authentication( $info, $this->config, $this->disable_login, $this->login_log );
    6666        $this->server_error_notification  = new CloudSecureWP_Server_Error_Notification( $info, $this->config );
    6767        $this->waf                        = new CloudSecureWP_Waf( $info, $this->config );
     
    145145
    146146            add_action( 'wp_login', array( $this->login_log, 'wp_login' ), 1, 1 );
     147            add_action( 'wp_login', array( $this->two_factor_authentication, 'cleanup_expired_sessions' ), 2, 0 );
    147148            add_action( 'xmlrpc_call', array( $this->login_log, 'xmlrpc_call' ), 10 );
    148149            add_action( 'wp_login_failed', array( $this->login_log, 'wp_login_failed' ), 20, 1 );
     
    258259
    259260            if ( $this->two_factor_authentication->is_enabled() && 'xmlrpc.php' !== basename( $_SERVER['SCRIPT_NAME'] ) && ! is_admin() ) {
    260                 add_filter( 'authenticate', array( $this->two_factor_authentication, 'decode_base64_credentials' ), 0, 3 );
    261                 add_action( 'wp_login', array( $this->two_factor_authentication, 'wp_login' ), 0, 2 );
     261                add_filter( 'sanitize_user', array( $this->two_factor_authentication, 'restore_login_name' ), 0, 1 );
     262                add_filter( 'authenticate', array( $this->two_factor_authentication, 'restore_login_session' ), 0, 3 );
     263                add_filter( 'authenticate', array( $this->two_factor_authentication, 'authenticate_with_two_factor' ), 100, 3 );
    262264                add_action( 'wp_login', array( $this->two_factor_authentication, 'redirect_if_not_two_factor_authentication_registered' ), 10, 2 );
    263265            }
  • cloudsecure-wp-security/trunk/modules/two-factor-authentication.php

    r3353611 r3422588  
    66
    77class CloudSecureWP_Two_Factor_Authentication extends CloudSecureWP_Common {
    8     private const KEY_FEATURE = 'two_factor_authentication';
     8    private const KEY_FEATURE        = 'two_factor_authentication';
     9    private const OPTION_PREFIX      = 'cloudsecurewp_2fa_data_';
     10    private const SESSION_EXPIRY     = 300;
     11    private const CLEANUP_TIMEOUT    = 60;
     12    private const CLEANUP_BATCH_SIZE = 1000;
    913
    1014    private $config;
     
    1418     */
    1519    private $disable_login;
    16    
    17     /**
    18      * 元の認証情報を保存(Base64デコード済み)
    19      */
    20     private $original_credentials = array();
    21 
    22     function __construct( array $info, CloudSecureWP_Config $config, CloudSecureWP_Disable_Login $disable_login ) {
     20
     21    /**
     22     * @var CloudSecureWP_Login_Log
     23     */
     24    private $login_log;
     25
     26    function __construct( array $info, CloudSecureWP_Config $config, CloudSecureWP_Disable_Login $disable_login, CloudSecureWP_Login_Log $login_log ) {
    2327        parent::__construct( $info );
    2428        $this->config        = $config;
    2529        $this->disable_login = $disable_login;
     30        $this->login_log     = $login_log;
    2631    }
    2732
     
    135140    private function is_role_enabled( $role ): bool {
    136141        return in_array( $role, get_option( 'cloudsecurewp_two_factor_authentication_roles', array() ) );
    137     }
    138 
    139     /**
    140      * ログインフォーム2段階認証チェック
    141      */
    142     public function wp_login( $user_login, $user ) {
    143         // 2段階認証が無効なとき
    144         if ( ! $this->is_enabled() ) {
    145             return;
    146         }
    147 
    148         if ( ! isset( $user->roles[0] ) ) {
    149             return;
    150         }
    151 
    152         // 有効な権限グループに含まれないとき
    153         if ( ! $this->is_role_enabled( $user->roles[0] ) ) {
    154             return;
    155         }
    156 
    157         $secret = get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user->ID );
    158         // ユーザーがデバイス登録をしていないとき
    159         if ( ! $secret ) {
    160             return;
    161         }
    162 
    163         // 初回ログイン時に元の認証情報を保存
    164         if ( empty( $this->original_credentials ) ) {
    165             $this->original_credentials['log'] = $user_login;
    166             $this->original_credentials['pwd'] = $_POST['pwd'] ?? '';
    167         }
    168 
    169         // 2段階認証コードが送られたとき
    170         if ( ! empty( $_POST['google_authenticator_code'] ) && check_admin_referer( $this->get_feature_key() . '_csrf' ) ) {
    171             $google_authenticator_code = sanitize_text_field( $_POST['google_authenticator_code'] );
    172             // 2段階認証コードが有効なとき
    173             if ( CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret, $google_authenticator_code, 2 ) ) {
    174                 return;
    175             }
    176             // ログイン失敗回数をインクリメントしデータベースに格納
    177             $this->disable_login->wp_login_failed( $user_login );
    178         }
    179 
    180         wp_logout();
    181         login_header( '2段階認証画面' );
    182         $this->login_error();
    183         $this->login_form();
    184         login_footer();
    185         exit;
    186142    }
    187143
     
    205161     * 2段階認証のログインフォームを出力
    206162     *
    207      * @return void
    208      */
    209     private function login_form() {
     163     * @param string $login_token
     164     *
     165     * @return void
     166     */
     167    private function login_form( $login_token ) {
    210168        ?>
    211169        <form name="loginform" id="loginform"
    212170                action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post">
    213             <input type="hidden" name="log" value="<?php echo base64_encode( $this->original_credentials['log'] ?? $_REQUEST['log'] ?? '' ); ?>"/>
    214             <input type="hidden" name="pwd" value="<?php echo base64_encode( $this->original_credentials['pwd'] ?? $_REQUEST['pwd'] ?? '' ); ?>"/>
    215             <?php if ( array_key_exists( 'cloudsecurewp_captcha', $_REQUEST ) ) : ?>
    216                 <input type="hidden" name="cloudsecurewp_captcha"
    217                         value="<?php echo esc_attr( sanitize_text_field( $_REQUEST['cloudsecurewp_captcha'] ) ); ?>"/>
    218             <?php endif; ?>
    219             <?php if ( array_key_exists( 'cloudsecurewp_captcha_prefix', $_REQUEST ) ) : ?>
    220                 <input type="hidden" name="cloudsecurewp_captcha_prefix"
    221                         value="<?php echo esc_attr( sanitize_text_field( $_REQUEST['cloudsecurewp_captcha_prefix'] ) ); ?>"/>
    222             <?php endif; ?>
    223             <?php if ( array_key_exists( 'cloudsecurewp_captcha_wpnonce', $_REQUEST ) ) : ?>
    224                 <input type="hidden" name="cloudsecurewp_captcha_wpnonce"
    225                         value="<?php echo esc_attr( sanitize_text_field( $_REQUEST['cloudsecurewp_captcha_wpnonce'] ) ); ?>"/>
    226             <?php endif; ?>
    227171            <?php if ( array_key_exists( 'rememberme', $_REQUEST ) && 'forever' === sanitize_text_field( $_REQUEST['rememberme'] ) ) : ?>
    228172                <input name="rememberme" type="hidden" id="rememberme" value="forever"/>
    229173            <?php endif; ?>
     174            <input type="hidden" name="login_token" value="<?php echo esc_attr( $login_token ); ?>">
    230175            <p>
    231176                <label for="google_authenticator_code">認証コード</label>
    232177                <input type="text" name="google_authenticator_code" id="google_authenticator_code" class="input"
    233                         value="" size="20"/>
     178                        value="" size="20" autocomplete="one-time-code"/>
    234179            </p>
    235180            <script type="text/javascript">document.getElementById("google_authenticator_code").focus();</script>
     
    296241
    297242    /**
    298      * 2段階認証フォームからのBase64エンコードされた認証情報をデコード
     243     * ユーザの2faシークレットキー取得
     244     *
     245     * @param int $user_id
     246     *
     247     * @return mixed
     248     */
     249    private function get_2fa_secret_key( int $user_id ) {
     250        return get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user_id );
     251    }
     252
     253    /**
     254     * option keyを作成
     255     *
     256     * @param string $token
     257     *
     258     * @return string
     259     */
     260    private function create_option_key( string $token ): string {
     261        return self::OPTION_PREFIX . $token;
     262    }
     263
     264    /**
     265     * option dataを登録
     266     *
     267     * @param string $key
     268     * @param mixed  $data
     269     *
     270     * @return void
     271     */
     272    private function set_option_data( string $key, $data ): void {
     273        update_option( $key, $data, false );
     274    }
     275
     276    /**
     277     * option dataを取得
     278     *
     279     * @param string $key
     280     *
     281     * @return array|false データが存在しないまたは、有効期限切れの場合FALSEを返却
     282     */
     283    private function get_option_data( string $key ) {
     284
     285        $data = get_option( $key );
     286
     287        // データが存在しない
     288        if ( ! $data || ! is_array( $data ) ) {
     289            return false;
     290        }
     291
     292        // 有効期限切れ
     293        if ( ! isset( $data['expires'] ) || $data['expires'] <= time() ) {
     294            return false;
     295        }
     296
     297        // 有効なデータを返却
     298        return $data;
     299    }
     300
     301    /**
     302     * option dataを削除
     303     *
     304     * @param string $key
     305     *
     306     * @return void
     307     */
     308    private function delete_option_data( string $key ): void {
     309        delete_option( $key );
     310    }
     311
     312    /**
     313     * 2段階認証が必要かどうか判定処理
    299314     *
    300315     * @param mixed $user
     316     *
     317     * @return bool
     318     */
     319    private function is_2fa_required( $user ): bool {
     320
     321        // 2段階認証が無効な場合
     322        if ( ! $this->is_enabled() ) {
     323            return false;
     324        }
     325
     326        // 有効な権限グループに含まれない場合
     327        if ( ! isset( $user->roles[0] ) || ! $this->is_role_enabled( $user->roles[0] ) ) {
     328            return false;
     329        }
     330
     331        // 2faシークレットキーが存在しない場合
     332        if ( ! $this->get_2fa_secret_key( $user->ID ) ) {
     333            return false;
     334        }
     335
     336        return true;
     337    }
     338
     339    /**
     340     * 2段階認証画面を表示
     341     *
     342     * @param string $login_token
     343     *
     344     * @return void
     345     */
     346    private function show_two_factor_form( string $login_token ) {
     347        // 2FA画面を表示
     348        login_header( '2段階認証画面' );
     349        $this->login_error();
     350        $this->login_form( $login_token );
     351        login_footer();
     352        exit;
     353    }
     354
     355    /**
     356     * POSTデータから2FA関連の値を安全に取得
     357     *
     358     * @return array
     359     */
     360    private function get_2fa_post_data(): array {
     361        return array(
     362            'login_token'                => sanitize_text_field( $_POST['login_token'] ?? '' ),
     363            'google_authenticator_code'  => sanitize_text_field( $_POST['google_authenticator_code'] ?? '' ),
     364        );
     365    }
     366
     367    /**
     368     * 2段階認証コード検証処理
     369     *
     370     * @param int    $user_id
     371     * @param string $code
     372     *
     373     * @return bool
     374     */
     375    private function verify_2fa_code( int $user_id, string $code ): bool {
     376
     377        // 2faシークレットキー取得
     378        $secret_key = $this->get_2fa_secret_key( $user_id );
     379
     380        // 2faシークレットキーが存在しない場合
     381        if ( ! $secret_key ) {
     382            return true;
     383        }
     384
     385        // 2段階認証コードが有効な場合
     386        if ( CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret_key, $code, 2 ) ) {
     387            return true;
     388        }
     389
     390        // 認証失敗
     391        return false;
     392    }
     393
     394    /**
     395     * 認証フック: ユーザ名復元処理
     396     *
     397     * @param string $username
     398     * @param bool   $strict
     399     *
     400     * @return string
     401     */
     402    public function restore_login_name( string $username, $strict = false ): string {
     403
     404        // 初回アクセス・または初回認証の場合、何もしない
     405        if ( ! isset( $_POST['google_authenticator_code'] ) ) {
     406            return $username;
     407        }
     408
     409        // 2FA関連のPOSTデータ取得
     410        $post_data = $this->get_2fa_post_data();
     411
     412        // ログイン情報を取得
     413        $option_key  = $this->create_option_key( $post_data['login_token'] );
     414        $option_data = $this->get_option_data( $option_key );
     415
     416        // ログイン情報が存在しない場合、何もしない
     417        if ( $option_data === false ) {
     418            return $username;
     419        }
     420
     421        // ユーザ名を返却
     422        return $option_data['user_login'];
     423    }
     424
     425    /**
     426     * 認証フック: ログインデータ復元処理
     427     *
     428     * @param mixed  $user
    301429     * @param string $username
    302430     * @param string $password
     431     *
    303432     * @return mixed
    304433     */
    305     public function decode_base64_credentials( $user, $username, $password ) {
    306         // 2段階認証フォームからの送信かチェック
    307         if ( ! empty( $_POST['google_authenticator_code'] ) && check_admin_referer( $this->get_feature_key() . '_csrf' ) ) {
    308             // Base64エンコードされた認証情報をデコード
    309             if ( isset( $_POST['log'] ) ) {
    310                 $decoded_username = base64_decode( $_POST['log'] );
    311                 $this->original_credentials['log'] = $decoded_username;
    312                 $_POST['log'] = $decoded_username;
    313             }
    314            
    315             if ( isset( $_POST['pwd'] ) ) {
    316                 $decoded_password = base64_decode( $_POST['pwd'] );
    317                 $this->original_credentials['pwd'] = $decoded_password;
    318                 $_POST['pwd'] = $decoded_password;
    319                
    320                 // デコードされたパスワードで認証を実行
    321                 return wp_authenticate_username_password( null, $decoded_username, $decoded_password );
    322             }
    323         }
    324        
     434    public function restore_login_session( $user, $username, $password ) {
     435
     436        // 初回アクセス・または初回認証の場合、何もしない
     437        if ( ! isset( $_POST['google_authenticator_code'] ) ) {
     438            return $user;
     439        }
     440
     441        // CSRFトークンを検証(失敗すると「辿ったリンクは期限が切れています。」のエラー画面を表示し処理終了)
     442        check_admin_referer( $this->get_feature_key() . '_csrf' );
     443
     444        // 2FA関連のPOSTデータ取得
     445        $post_data = $this->get_2fa_post_data();
     446
     447        // ログイン情報を取得
     448        $option_key  = $this->create_option_key( $post_data['login_token'] );
     449        $option_data = $this->get_option_data( $option_key );
     450
     451        // ログインが有効期限切れの場合
     452        // ログイン成功時にまとめてクリーンアップ処理を実行するため、ここでは消さない
     453        if ( $option_data === false ) {
     454            return new WP_Error( 'empty_username', 'セッションの有効期限が切れました。再度ログインしてください。' );
     455        }
     456
     457        // ユーザーオブジェクト取得
     458        $user = get_user_by( 'id', $option_data['user_id'] );
     459        if ( ! $user ) {
     460            $this->delete_option_data( $option_key );
     461            return new WP_Error( 'empty_username', 'ユーザー情報が見つかりません。再度ログインしてください。' );
     462        }
     463
     464        // ログイン情報のPOSTデータ復元
     465        $_POST['log'] = $option_data['user_login'];
     466
    325467        return $user;
    326468    }
     469
     470    /**
     471     * 認証フック: 2段階認証処理
     472     *
     473     * @param mixed  $user
     474     * @param string $username
     475     * @param string $password
     476     *
     477     * @return mixed
     478     */
     479    public function authenticate_with_two_factor( $user, $username, $password ) {
     480
     481        // 初回アクセス、または初回認証時
     482        if ( ! isset( $_POST['google_authenticator_code'] ) ) {
     483
     484            // 認証失敗の場合
     485            if ( is_wp_error( $user ) ) {
     486                return $user;
     487            }
     488
     489            // 2段階認証が不要な場合
     490            if ( ! $this->is_2fa_required( $user ) ) {
     491                return $user;
     492            }
     493
     494            // option key生成
     495            $session_token = bin2hex( random_bytes( 16 ) );
     496            $option_key    = $this->create_option_key( $session_token );
     497
     498            // 保存用認証データ作成
     499            $option_data = array(
     500                'user_id'    => $user->ID,
     501                'user_login' => sanitize_text_field( $_POST['log'] ?? '' ),
     502                'expires'    => time() + self::SESSION_EXPIRY,
     503                'created'    => time(),
     504            );
     505
     506            // データを保存
     507            $this->set_option_data( $option_key, $option_data );
     508
     509            // 2FA画面を表示して、処理終了
     510            $this->show_two_factor_form( $session_token );
     511        }
     512
     513        // 2FA関連のPOSTデータ取得
     514        $post_data = $this->get_2fa_post_data();
     515
     516        // option key取得
     517        $option_key = $this->create_option_key( $post_data['login_token'] );
     518
     519        // $userがWP_Errorの場合は処理をスキップ
     520        if ( is_wp_error( $user ) ) {
     521            return $user;
     522        }
     523
     524        // 2段階認証成功の場合
     525        if ( $this->verify_2fa_code( $user->ID, $post_data['google_authenticator_code'] ) ) {
     526            $this->delete_option_data( $option_key );
     527            return $user;
     528        }
     529
     530        // ログイン失敗時の処理を実行(ログイン回数・ログインログ)
     531        do_action( 'wp_login_failed', $_POST['log'], $user );
     532
     533        // 2FA画面を再表示して、処理終了
     534        $this->show_two_factor_form( $post_data['login_token'] );
     535    }
     536
     537    /**
     538     * 2FAセッションデータを取得
     539     *
     540     * @param int $last_option_id
     541     * @param int $limit
     542     *
     543     * @return array
     544     */
     545    private function fetch_2fa_sessions( int $last_option_id, int $limit ): array {
     546        global $wpdb;
     547
     548        // セッションキーの接頭辞でLIKE検索
     549        $like = $wpdb->esc_like( self::OPTION_PREFIX ) . '%';
     550
     551        // SQL実行(LIKE検索を行うため、意図的に直接クエリを実行する)
     552        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     553        $options = $wpdb->get_results(
     554            $wpdb->prepare(
     555                "SELECT
     556                    option_id,
     557                    option_name,
     558                    option_value
     559                FROM
     560                    {$wpdb->options}
     561                WHERE TRUE
     562                    AND option_name LIKE %s
     563                    AND %d < option_id
     564                ORDER BY
     565                    option_id ASC
     566                LIMIT
     567                    %d",
     568                $like,
     569                $last_option_id,
     570                $limit
     571            )
     572        );
     573
     574        return $options ?? array();
     575    }
     576
     577    /**
     578     * 期限切れセッションデータを収集
     579     *
     580     * @param array $options
     581     *
     582     * @return array ['log_data' => array, 'delete_option_names' => array]
     583     */
     584    private function collect_expired_session_data( array $options ): array {
     585        // ログ登録用データリスト初期化
     586        $log_data = array();
     587        // 削除対象のoption_nameリスト
     588        $delete_option_names = array();
     589
     590        foreach ( $options as $option ) {
     591            // option_valueを連想配列に変換
     592            $data = maybe_unserialize( $option->option_value );
     593
     594            // 配列でない場合はスキップ
     595            if ( ! is_array( $data ) ) {
     596                continue;
     597            }
     598
     599            // 有効期限が切れている場合
     600            if ( isset( $data['expires'] ) && $data['expires'] <= time() ) {
     601
     602                // ログ登録用データを収集
     603                $log_data[] = array(
     604                    'name'     => $data['user_login'],
     605                    'ip'       => $this->get_client_ip( '' ),
     606                    'status'   => self::LOGIN_STATUS_FAILED,
     607                    'method'   => 1,
     608                    'login_at' => wp_date( 'Y-m-d H:i:s', $data['created'] ), // WPのタイムゾーンに変更して登録
     609                );
     610
     611                // 削除対象のoption_nameを収集
     612                $delete_option_names[] = $option->option_name;
     613            }
     614        }
     615
     616        return array(
     617            'log_data'            => $log_data,
     618            'delete_option_names' => $delete_option_names,
     619        );
     620    }
     621
     622    /**
     623     * ログイン失敗ログを一括登録
     624     * (呼び出し元でトランザクションを管理すること)
     625     *
     626     * @param array $log_data
     627     *
     628     * @return void
     629     * @throws Exception SQLエラー発生時.
     630     */
     631    private function insert_login_failed_logs( array $log_data ): void {
     632        if ( empty( $log_data ) ) {
     633            return;
     634        }
     635
     636        global $wpdb;
     637
     638        // プレースホルダーと値の準備
     639        $values       = array();
     640        $placeholders = array();
     641
     642        // 収集したログデータを一括登録用に変換
     643        foreach ( $log_data as $log ) {
     644            $values[]       = $log['name'];
     645            $values[]       = $log['ip'];
     646            $values[]       = $log['status'];
     647            $values[]       = $log['method'];
     648            $values[]       = $log['login_at'];
     649            $placeholders[] = '(%s, %s, %d, %d, %s)';
     650        }
     651
     652        // SQL実行(一括登録を行うため、意図的に直接クエリを実行する)
     653        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     654        $result = $wpdb->query(
     655            $wpdb->prepare(
     656                "INSERT INTO `{$wpdb->prefix}cloudsecurewp_login_log`
     657                (`name`, `ip`, `status`, `method`, `login_at`)
     658                VALUES " . implode( ', ', $placeholders ),
     659                $values
     660            )
     661        );
     662
     663        // SQLエラーチェック
     664        if ( $result === false || ! empty( $wpdb->last_error ) ) {
     665            throw new Exception( 'Failed to insert login logs: ' . $wpdb->last_error );
     666        }
     667    }
     668
     669    /**
     670     * 指定されたオプションを一括削除
     671     * (呼び出し元でトランザクションを管理すること)
     672     *
     673     * @param array $option_names
     674     *
     675     * @return void
     676     * @throws Exception SQLエラー発生時.
     677     */
     678    private function delete_options( array $option_names ): void {
     679        if ( empty( $option_names ) ) {
     680            return;
     681        }
     682
     683        global $wpdb;
     684
     685        // プレースホルダー作成
     686        $placeholders = implode( ', ', array_fill( 0, count( $option_names ), '%s' ) );
     687
     688        // SQL実行(一括削除を行うため、意図的に直接クエリを実行する)
     689        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     690        $result = $wpdb->query(
     691            $wpdb->prepare(
     692                "DELETE FROM
     693                    {$wpdb->options}
     694                WHERE
     695                    option_name IN ($placeholders)",
     696                $option_names
     697            )
     698        );
     699
     700        // SQLエラーチェック
     701        if ( $result === false || ! empty( $wpdb->last_error ) ) {
     702            throw new Exception( 'Failed to delete options: ' . $wpdb->last_error );
     703        }
     704    }
     705
     706    /**
     707     * 期限切れログイン情報セッションのクリーンアップ処理本体
     708     *
     709     * @return void
     710     */
     711    private function process_cleanup_expired_sessions(): void {
     712        global $wpdb;
     713
     714        $last_option_id = 0;
     715
     716        while ( true ) {
     717            try {
     718                // 2FAセッションデータを取得
     719                $options = $this->fetch_2fa_sessions( $last_option_id, self::CLEANUP_BATCH_SIZE );
     720
     721                // 取得するレコードがなくなったら終了
     722                if ( empty( $options ) ) {
     723                    break;
     724                }
     725
     726                // 最後に取得したoption_idを更新
     727                $last_option_id = end( $options )->option_id;
     728
     729                // 期限切れセッションデータを収集
     730                $result              = $this->collect_expired_session_data( $options );
     731                $log_data            = $result['log_data'];
     732                $delete_option_names = $result['delete_option_names'];
     733
     734                if ( empty( $delete_option_names ) && empty( $log_data ) ) {
     735                    continue;
     736                }
     737
     738                // トランザクション開始
     739                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     740                $wpdb->query( 'START TRANSACTION' );
     741
     742                // ログデータを一括登録
     743                $this->insert_login_failed_logs( $log_data );
     744
     745                // オプションを一括削除
     746                $this->delete_options( $delete_option_names );
     747
     748                // トランザクションコミット
     749                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     750                $wpdb->query( 'COMMIT' );
     751
     752            } catch ( Exception $e ) {
     753                // エラー発生時はロールバック
     754                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     755                $wpdb->query( 'ROLLBACK' );
     756                break;
     757            }
     758        }
     759    }
     760
     761    /**
     762     * 期限切れの2FAセッションをクリーンアップ
     763     *
     764     * @return void
     765     */
     766    public function cleanup_expired_sessions(): void {
     767        global $wpdb;
     768
     769        // ロック名
     770        $lock_name = 'cloudsecurewp_2fa_cleanup_lock';
     771        // クリーンアップ処理の完了を待つ最大秒数
     772        $timeout = self::CLEANUP_TIMEOUT;
     773
     774        // ロックを取得
     775        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     776        $get_lock = $wpdb->get_var(
     777            $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lock_name )
     778        );
     779
     780        if ( $get_lock === '1' ) {
     781            // ロック取得成功(クリーンアップ実行者)
     782
     783            try {
     784                // クリーンアップ処理実行
     785                $this->process_cleanup_expired_sessions();
     786            } catch ( Exception $e ) {
     787                // クリーンアップ処理実行で失敗しても内部でロールバックするため、ここでは何もしない
     788            } finally {
     789                // ロック解放
     790                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     791                $wpdb->query(
     792                    $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name )
     793                );
     794            }
     795        } else {
     796            // ロック取得失敗(待機者)
     797
     798            // リーダーのクリーンアップ完了を待機
     799            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     800            $acquired_signal = $wpdb->get_var(
     801                $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $lock_name, $timeout )
     802            );
     803
     804            if ( $acquired_signal === '1' ) {
     805                // ロック取得成功(クリーンアップ処理終了)
     806                // 即座にロック解放(待機完了の合図として使うだけ)
     807                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
     808                $wpdb->query(
     809                    $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name )
     810                );
     811            }
     812        }
     813    }
    327814}
  • cloudsecure-wp-security/trunk/readme.txt

    r3408912 r3422588  
    44Requires at least: 5.3.15
    55Tested up to: 6.9
    6 Stable tag: 1.3.21
     6Stable tag: 1.3.22
    77License: GPLv2 or later
    88License URI: http://www.gnu.org/licenses/gpl-2.0.html
     
    107107== Changelog ==
    108108
     109= 1.3.22 =
     110* 2段階認証に関する軽微な修正
     111
    109112= 1.3.21 =
    110113* WordPress6.9をサポート
Note: See TracChangeset for help on using the changeset viewer.