Changeset 3462167
- Timestamp:
- 02/16/2026 03:03:52 AM (4 days ago)
- Location:
- cloudsecure-wp-security/trunk
- Files:
-
- 3 added
- 14 edited
-
assets/css/style.css (modified) (6 diffs)
-
assets/images/icon_mail.svg (added)
-
assets/images/icon_mobile.svg (added)
-
cloudsecure-wp.php (modified) (1 diff)
-
modules/admin/two-factor-authentication-registration.php (modified) (6 diffs)
-
modules/admin/two-factor-authentication.php (modified) (3 diffs)
-
modules/cloudsecure-wp.php (modified) (5 diffs)
-
modules/common.php (modified) (1 diff)
-
modules/disable-access-system-file.php (modified) (1 diff)
-
modules/lib/class-recovery-codes.php (added)
-
modules/lib/class-time-based-one-time-password.php (modified) (5 diffs)
-
modules/login-log.php (modified) (1 diff)
-
modules/two-factor-authentication.php (modified) (21 diffs)
-
modules/waf-engine.php (modified) (10 diffs)
-
modules/waf.php (modified) (6 diffs)
-
readme.txt (modified) (2 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
cloudsecure-wp-security/trunk/assets/css/style.css
r3317930 r3462167 134 134 /* エラーメッセージと完了メッセージ */ 135 135 #cloudsecure-wp-security .error-box, 136 #cloudsecure-wp-security .success-box { 136 #cloudsecure-wp-security .success-box, 137 #cloudsecure-wp-security .info-box { 137 138 position: relative; 138 139 margin-bottom: 12px; … … 150 151 151 152 #cloudsecure-wp-security .error-box:last-child, 152 #cloudsecure-wp-security .success-box:last-child { 153 #cloudsecure-wp-security .success-box:last-child, 154 #cloudsecure-wp-security .info-box:last-child { 153 155 margin-bottom: 24px; 154 156 } 155 157 156 158 #cloudsecure-wp-security .error-box::after, 157 #cloudsecure-wp-security .success-box::after { 159 #cloudsecure-wp-security .success-box::after, 160 #cloudsecure-wp-security .info-box::after { 158 161 position: absolute; 159 162 top: 0; … … 172 175 } 173 176 177 #cloudsecure-wp-security .info-box::after { 178 background-color: #72aee6; 179 } 180 174 181 #cloudsecure-wp-security .error-box.red { 175 182 background-color: #fbefef; … … 178 185 #cloudsecure-wp-security .success-box.green { 179 186 background-color: #ebf8ee; 187 } 188 189 #cloudsecure-wp-security .info-box.blue { 190 background-color: #f0f6fc; 180 191 } 181 192 … … 243 254 244 255 #cloudsecure-wp-security .box-row.pb-0 { 245 padding-bottom: 0;256 padding-bottom: 0; 246 257 } 247 258 248 259 #cloudsecure-wp-security .box-row.flex-start { 249 align-items: flex-start;260 align-items: flex-start; 250 261 } 251 262 252 263 #cloudsecure-wp-security .box-row:last-child { 253 border: none;264 border: none; 254 265 } 255 266 … … 1053 1064 } 1054 1065 } 1066 /* 以下 2段階認証の認証方法設定ページのスタイル */ 1067 /* ボタン関連 */ 1068 #two-fa-setting-area .button { 1069 font-size: 14px !important; 1070 line-height: 0 !important; 1071 min-height: 32px !important; 1072 } 1073 #two-fa-setting-area .button-gray { 1074 background-color: #b7b9bc !important; 1075 border-color: #b7b9bc !important; 1076 color: #fff !important; 1077 } 1078 1079 #two-fa-setting-area .button-gray:hover { 1080 background-color: #9fa2a6 !important; 1081 border-color: #9fa2a6 !important; 1082 } 1083 1084 #two-fa-setting-area .button-blue { 1085 background-color: #329FF1 !important; 1086 border-color: #329FF1 !important; 1087 color: #fff !important; 1088 } 1089 #two-fa-setting-area .button-blue:hover { 1090 background-color: #2088D6 !important; 1091 border-color: #2088D6 !important; 1092 } 1093 /* コンテンツスタイル関連 */ 1094 #cloudsecure-wp-security #two-fa-setting-area .box { 1095 margin: 0 !important; 1096 padding: 0 !important; 1097 } 1098 #cloudsecure-wp-security #two-fa-setting-area .box-bottom { 1099 padding: 0 24px !important; 1100 } 1101 #cloudsecure-wp-security #two-fa-setting-area .box-row { 1102 padding: 24px 0 !important; 1103 } 1104 #cloudsecure-wp-security #two-fa-setting-area .box-row-title { 1105 display: flex; 1106 flex-direction: column; 1107 gap: 4px; 1108 } 1109 #cloudsecure-wp-security #two-fa-setting-area .status-area { 1110 display: flex; 1111 justify-content: space-between; 1112 align-items: center; 1113 } 1114 /* リカバリーコード説明 */ 1115 #cloudsecure-wp-security #two-fa-setting-area .description-recovery-code { 1116 width: 211px; 1117 padding: 6px 12px; 1118 border-radius: 4px; 1119 background-color: #F7F7F8; 1120 } 1121 #cloudsecure-wp-security #two-fa-setting-area .description-text { 1122 margin: 0; 1123 font-size: 11px; 1124 font-weight: 400; 1125 color: #646970; 1126 } 1127 /* アイコン付きステータス表示エリア */ 1128 #two-fa-setting-area .status-registered-back-none { 1129 display: inline-flex; 1130 align-items: center; 1131 gap: 4px; 1132 color: #00A32A; 1133 } 1134 #two-fa-setting-area .status-not-registered-back-none { 1135 display: inline-flex; 1136 align-items: center; 1137 gap: 8px; 1138 color: #d73a49; 1139 } 1140 /* モーダルスタイル関連 */ 1141 #two-fa-setting-area .setting-modal, 1142 #two-fa-setting-area .confirm-modal { 1143 display: none; 1144 position: fixed; 1145 z-index: 99999; 1146 left: 0; 1147 top: 0; 1148 width: 100%; 1149 height: 100dvh; 1150 background-color: rgba(0, 0, 0, 0.5); 1151 } 1152 #two-fa-setting-area .setting-modal-content, 1153 #two-fa-setting-area .confirm-modal-content { 1154 position: absolute; 1155 top: 50%; 1156 left: 50%; 1157 transform: translate(-50%, -50%); 1158 background-color: #fff; 1159 border-radius: 8px; 1160 width: 90%; 1161 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 1162 } 1163 #two-fa-setting-area .setting-modal-content { 1164 max-width: 650px; 1165 } 1166 #two-fa-setting-area .confirm-modal-content { 1167 max-width: 400px; 1168 } 1169 #two-fa-setting-area .setting-modal-header { 1170 display: flex; 1171 justify-content: space-between; 1172 align-items: center; 1173 padding: 16px; 1174 border-bottom: 1px solid #DBDCDD; 1175 } 1176 #two-fa-setting-area .setting-modal-body { 1177 display: flex; 1178 flex-direction: column; 1179 gap: 24px; 1180 padding: 24px 24px 32px 24px; 1181 } 1182 #two-fa-setting-area .confirm-modal-body { 1183 display: flex; 1184 flex-direction: column; 1185 gap: 32px; 1186 padding: 24px; 1187 } 1188 #two-fa-setting-area .confirm-message-area { 1189 display: flex; 1190 flex-direction: column; 1191 gap: 8px; 1192 } 1193 #two-fa-setting-area .confirm-message-body { 1194 display: flex; 1195 flex-direction: column; 1196 gap: 4px; 1197 } 1198 #two-fa-setting-area .modal-btn-area-end{ 1199 display: flex; 1200 justify-content: flex-end; 1201 gap: 16px; 1202 } 1203 #two-fa-setting-area .modal-btn-area-center{ 1204 display: flex; 1205 justify-content: center; 1206 gap: 16px; 1207 } 1208 #two-fa-setting-area .modal-close { 1209 cursor: pointer; 1210 } 1211 #cloudsecure-wp-security #two-fa-setting-area h2, 1212 #two-fa-setting-area .h2-text { 1213 margin: 0; 1214 font-size: 14px; 1215 } 1216 #two-fa-setting-area .modal-text, 1217 #two-fa-setting-area .status-text { 1218 font-size: 13px !important; 1219 margin: 0 !important; 1220 } 1221 #cloudsecure-wp-security #setting-modal .info-box, 1222 #cloudsecure-wp-security #setting-modal .error-box { 1223 margin: 0 !important; 1224 font-size: 13px !important; 1225 } 1226 /* 認証方法選択画面モーダル関連 */ 1227 .auth-method-options { 1228 display: flex; 1229 flex-direction: column; 1230 gap: 16px; 1231 } 1232 #cloudsecure-wp-security #two-fa-setting-area .circle-radio { 1233 margin: 0 !important; 1234 flex-shrink: 0 !important; 1235 } 1236 #cloudsecure-wp-security #two-fa-setting-area .circle-radio+label { 1237 cursor: pointer !important; 1238 } 1239 #cloudsecure-wp-security #two-fa-setting-area .circle-radio+label::after { 1240 position: absolute !important; 1241 top: 50% !important; 1242 transform: translateY(-50%) !important; 1243 } 1244 1245 #cloudsecure-wp-security #two-fa-setting-area .circle-radio+label::before { 1246 position: absolute !important; 1247 top: 50% !important; 1248 transform: translateY(-50%) !important; 1249 } 1250 #two-fa-setting-area .auth-method-icon, 1251 #two-fa-setting-area .auth-method-text { 1252 vertical-align: middle; 1253 } 1254 /* アプリ認証モーダル関連 */ 1255 #two-fa-setting-area .qr-setup-container { 1256 display: flex; 1257 gap: 32px; 1258 padding-left: 8px; 1259 } 1260 #two-fa-setting-area #qrcode { 1261 display: inline-block; 1262 } 1263 #two-fa-setting-area .setup-key-section { 1264 display: flex; 1265 flex-direction: column; 1266 gap: 24px; 1267 padding-top: 16px; 1268 } 1269 #two-fa-setting-area .input-code-section { 1270 display: flex; 1271 align-items: center; 1272 gap: 8px; 1273 } 1274 #two-fa-setting-area .verification-code-email::placeholder, 1275 #two-fa-setting-area .verification-code-app::placeholder { 1276 color: #cccccc; 1277 } 1278 #two-fa-setting-area .verification-code-app { 1279 width: 160px; 1280 height: 38px; 1281 padding: 4px 7px; 1282 text-align: left; 1283 border: 1px solid #DBDCDD; 1284 border-radius: 4px; 1285 } 1286 /* メール認証モーダル関連 */ 1287 #two-fa-setting-area .verification-code-email { 1288 width: 207px; 1289 height: 38px; 1290 padding: 4px 7px; 1291 text-align: left; 1292 border: 1px solid #DBDCDD; 1293 border-radius: 4px; 1294 } 1295 #two-fa-setting-area .resend-email-code-active { 1296 pointer-events: auto; 1297 color: #2271b1; 1298 cursor: pointer; 1299 } 1300 #two-fa-setting-area .resend-email-code-inactive { 1301 pointer-events: none; 1302 color: #646970; 1303 cursor: not-allowed; 1304 text-decoration: none; 1305 } 1306 /* リカバリーコードモーダル関連 */ 1307 #two-fa-setting-area .recovery-modal-body { 1308 display: flex; 1309 flex-direction: column; 1310 gap: 24px; 1311 padding-top: 24px; 1312 } 1313 #two-fa-setting-area .recovery-modal-body-top { 1314 display: flex; 1315 flex-direction: column; 1316 gap: 16px; 1317 padding: 0 24px; 1318 } 1319 #two-fa-setting-area .recovery-modal-body-bottom { 1320 display: flex; 1321 flex-direction: column; 1322 gap: 10px; 1323 padding: 16px 24px; 1324 background-color: #f0f0f1; 1325 border-radius: 0 0 8px 8px; 1326 } 1327 #two-fa-setting-area .recovery-text-area { 1328 display: flex; 1329 flex-direction: column; 1330 gap: 4px; 1331 } 1332 #two-fa-setting-area .recovery-text { 1333 display: flex; 1334 gap: 8px; 1335 } 1336 #two-fa-setting-area .recovery-codes-grid { 1337 display: grid; 1338 grid-template-columns: max-content max-content; 1339 justify-content: center; 1340 gap: 8px 20px; 1341 padding: 12px 0; 1342 border-radius: 8px; 1343 border: 1px solid #D2D2D2; 1344 } 1345 #two-fa-setting-area .recovery-code-item { 1346 background: none; 1347 border: none; 1348 font-family: 'BIZ UDGothic', monospace; 1349 font-size: 14px; 1350 text-align: center; 1351 letter-spacing: 1px; 1352 } 1353 #two-fa-setting-area .black-circle { 1354 margin-top: 7px; 1355 width: 4px; 1356 height: 4px; 1357 flex-shrink: 0; 1358 background-color: black; 1359 border-radius: 50%; 1360 display: inline-block; 1361 vertical-align: middle; 1362 } 1363 1364 /* タブレット対応 */ 1365 @media (max-width: 1024px) { 1366 #cloudsecure-wp-security #two-fa-setting-area .box-row { 1367 display: flex !important; 1368 flex-direction: column !important; 1369 column-gap: 20px !important; 1370 align-items: flex-start !important; 1371 } 1372 #cloudsecure-wp-security #two-fa-setting-area .box-row-title { 1373 flex-direction: row !important; 1374 width: 100% !important; 1375 align-items: center !important; 1376 } 1377 #cloudsecure-wp-security #two-fa-setting-area .box-row-content { 1378 width: 100% !important; 1379 } 1380 #two-fa-setting-area .pc-only { 1381 display: none; 1382 } 1383 #cloudsecure-wp-security #two-fa-setting-area .description-recovery-code { 1384 width: auto; 1385 display: inline-block; 1386 } 1387 } 1388 1389 /* スマートフォン対応 */ 1390 @media (max-width: 768px) { 1391 #cloudsecure-wp-security #two-fa-setting-area .box-row-title { 1392 flex-direction: column !important; 1393 width: 100% !important; 1394 align-items: flex-start !important; 1395 } 1396 #cloudsecure-wp-security #two-fa-setting-area .status-area { 1397 display: flex; 1398 flex-direction: column; 1399 gap: 16px; 1400 } 1401 #cloudsecure-wp-security #two-fa-setting-area .status-area-text, 1402 #cloudsecure-wp-security #two-fa-setting-area .status-area-btn { 1403 margin-right: auto !important; 1404 } 1405 #two-fa-setting-area .setting-modal-body { 1406 display: flex; 1407 flex-direction: column; 1408 gap: 24px; 1409 padding: 16px 16px 24px 16px; 1410 max-height: 510px; 1411 overflow: auto; 1412 } 1413 #two-fa-setting-area .qr-setup-container { 1414 display: flex; 1415 flex-direction: column; 1416 align-items: center; 1417 gap: 16px; 1418 } 1419 #two-fa-setting-area .setup-key-section { 1420 display: flex; 1421 flex-direction: column; 1422 gap: 16px; 1423 text-align: center; 1424 padding: 0; 1425 } 1426 #two-fa-setting-area .verification-code-email { 1427 width: 180px; 1428 height: 38px; 1429 padding: 4px 7px; 1430 text-align: left; 1431 border: 1px solid #DBDCDD; 1432 border-radius: 4px; 1433 } 1434 #two-fa-setting-area .recovery-modal-body { 1435 display: flex; 1436 flex-direction: column; 1437 gap: 0; 1438 padding: 0; 1439 } 1440 #two-fa-setting-area .recovery-modal-body-top { 1441 display: flex; 1442 flex-direction: column; 1443 gap: 24px; 1444 padding: 16px 16px 24px 16px; 1445 } 1446 } -
cloudsecure-wp-security/trunk/cloudsecure-wp.php
r3444508 r3462167 14 14 * Plugin URI: https://wpplugin.cloudsecure.ne.jp/cloudsecure_wp_security 15 15 * Description: 管理画面とログインURLをサイバー攻撃から守る、安心の国産・日本語対応プラグインです。かんたんな設定を行うだけで、不正アクセスや不正ログインからあなたのWordPressを保護し、セキュリティが向上します。また、各機能の有効・無効(ON・OFF)や設定などをお好みにカスタマイズし、いつでも保護状態を管理できます。 16 * Version: 1. 3.2416 * Version: 1.4.0 17 17 * Requires PHP: 7.1 18 18 * Author: CloudSecure,Inc. -
cloudsecure-wp-security/trunk/modules/admin/two-factor-authentication-registration.php
r3438297 r3462167 8 8 private $two_factor_authentication; 9 9 /** 10 * 管理画面にレンダリングするキー 10 * 認証方法 11 * 12 * @var bool 13 */ 14 private $auth_method; 15 /** 16 * リカバリーコードが登録済みかどうか 17 * 18 * @var bool 19 */ 20 private $is_registered_recovery; 21 /** 22 * 使用可能なリカバリーコードの数 23 * 24 * @var bool 25 */ 26 private $recovery_cnt; 27 /** 28 * ユーザのメールアドレス 11 29 * 12 30 * @var string 13 31 */ 14 private $default_key; 15 /** 16 * 秘密鍵が登録済みかどうか 17 * 18 * @var bool 19 */ 20 private $is_registered; 32 private $mail_address; 21 33 22 34 function __construct( array $info, CloudSecureWP_Two_Factor_Authentication $two_factor_authentication ) { … … 31 43 */ 32 44 public function prepare_view_data(): void { 33 $stored_secret = get_user_option( 'cloudsecurewp_two_factor_authentication_secret' ); 34 $this->is_registered = ! empty( $stored_secret ); 35 $this->default_key = ''; 36 if ( ! empty( $_POST ) && check_admin_referer( $this->two_factor_authentication->get_feature_key() . '_csrf' ) ) { 37 if ( ! empty( $_POST['key'] ) ) { 38 $this->default_key = sanitize_text_field( $_POST['key'] ); 39 } 40 if ( empty( $_POST['key'] ) ) { 41 $this->errors[] = 'セットアップキーを保存できませんでした。<br />セットアップキーを生成してください。'; 42 43 return; 44 } 45 if ( empty( $_POST['google_authenticator_code'] ) ) { 46 $this->errors[] = 'セットアップキーを保存できませんでした。<br />QRコードをGoogle Authenticator アプリケーションでスキャンし、認証コードを入力してください。'; 47 48 return; 49 } 50 $key = sanitize_text_field( $_POST['key'] ); 51 $google_authenticator_code = sanitize_text_field( $_POST['google_authenticator_code'] ); 52 if ( CloudSecureWP_Time_Based_One_Time_Password::verify_code( $key, $google_authenticator_code, 2 ) ) { 53 update_user_option( get_current_user_id(), 'cloudsecurewp_two_factor_authentication_secret', $key ); 54 $this->messages[] = 'セットアップキーを保存しました。'; 55 // 保存後は秘密鍵を非表示にする 56 $this->default_key = ''; 57 $this->is_registered = true; 58 } else { 59 $this->errors[] = 'セットアップキーを保存できませんでした。<br />認証コードが間違っています。'; 45 $user_id = get_current_user_id(); 46 $auth_info = $this->two_factor_authentication->get_2fa_auth_info( $user_id ); 47 48 $this->auth_method = $this->two_factor_authentication::USER_AUTH_METHOD_NONE; 49 $this->is_registered_recovery = false; 50 $this->recovery_cnt = 0; 51 $this->mail_address = $this->two_factor_authentication->mask_email( $user_id ); 52 53 if ( count( $auth_info ) === 0 ) { 54 $auth_info = $this->two_factor_authentication->repair_migration_gaps( $user_id ); 55 } 56 57 if ( count( $auth_info ) > 0 ) { 58 $this->auth_method = intVal( $auth_info['method'] ); 59 60 if ( ! is_null( $auth_info['recovery'] ) ) { 61 $this->is_registered_recovery = true; 62 $this->recovery_cnt = count( $auth_info['recovery'] ); 63 } 64 } 65 66 if ( isset( $_POST['message'] ) ) { 67 $message = sanitize_text_field( $_POST['message'] ) ?? ''; 68 if ( ! empty( $message ) ) { 69 $this->messages[] = $message; 70 } 71 } 72 if ( isset( $_POST['error'] ) ) { 73 $error = sanitize_text_field( $_POST['error'] ) ?? ''; 74 if ( ! empty( $error ) ) { 75 $this->errors[] = $error; 60 76 } 61 77 } … … 68 84 ?> 69 85 <div class="title-block mb-12"> 70 <h1 class="title-block-title"> デバイス登録 - 2段階認証</h1>86 <h1 class="title-block-title">2段階認証の設定</h1> 71 87 <p class="title-block-small-text">この機能のマニュアルは<a class="title-block-link" target="_blank" 72 88 href="https://wpplugin.cloudsecure.ne.jp/cloudsecure_wp_security/two_factor_authentication.php">こちら</a> … … 74 90 </div> 75 91 <div class="title-bottom-text"> 76 2段階認証 機能をアクティブにするには、セットアップキーを生成し、<a class="title-block-link" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a> アプリケーションでQRコードを読み込んでください。<br />77 QRコードを読み込めない場合、<a class="title-block-link" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a> アプリケーションにセットアップキーを入力してください。92 2段階認証に使用する認証方法を設定します。<br /> 93 Google Authenticator またはメール認証のいずれかを使用できます。 78 94 </div> 79 95 <?php … … 85 101 protected function page(): void { 86 102 ?> 87 < form method="post">103 <div id="two-fa-setting-area"> 88 104 <div class="box"> 89 105 <div class="box-bottom"> 90 <div class="box-row flex-start">91 <div class="box-row-title not-label pt-12">92 <label for="key">設定 状況</label>106 <div class="box-row"> 107 <div class="box-row-title not-label"> 108 <label for="key">設定方法</label> 93 109 </div> 94 110 <div class="box-row-content"> 95 <div class="flex"> 96 <?php if ( $this->is_registered ) : ?> 97 <p>設定済</p> 111 <div id="auth-method-status" class="status-area" data-auth-method="<?php echo esc_attr( $this->auth_method ); ?>"> 112 <?php if ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_APP ) : ?> 113 <div class="status-area-text"> 114 <p class="status-text">Authenticatorアプリ</p> 115 </div> 116 <?php elseif ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_EMAIL ) : ?> 117 <div class="status-area-text"> 118 <p class="status-text">メール認証</p> 119 </div> 120 <?php else : ?> 121 <div class="status-not-registered-back-none status-area-text"> 122 <span class="dashicons dashicons-warning"></span> 123 <p class="status-text">未設定</p> 124 </div> 125 <?php endif; ?> 126 <button id="register-method" type="button" class="status-area-btn button"> 127 <?php echo ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ? '設定' : '設定を変更' ); ?> 128 </button> 129 </div> 130 </div> 131 </div> 132 <div class="box-row"> 133 <div class="box-row-title not-label"> 134 <label for="key">リカバリーコード</label> 135 <div class="description-recovery-code"> 136 <p class="description-text">デバイス紛失時などの緊急時、<br class="pc-only">認証代わりに使用できるコードです。</p> 137 </div> 138 </div> 139 <div class="box-row-content"> 140 <div id="recovery-code-status" class="status-area" data-is-registered-recovery="<?php echo esc_attr( $this->is_registered_recovery ? 1 : 0 ); ?>"> 141 <div class="status-area-text"> 142 <?php if ( $this->is_registered_recovery ) : ?> 143 <p class="status-text">生成済み(残り<?php echo esc_html( $this->recovery_cnt ); ?>個)</p> 98 144 <?php else : ?> 99 <p>未設定</p> 145 <div class="status-not-registered-back-none status-area-text"> 146 <span class="dashicons dashicons-warning"></span> 147 <p class="status-text">未生成</p> 148 </div> 149 <?php if ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ) : ?> 150 <p class="status-text description">※認証方法の設定完了後、生成可能になります。</p> 151 <?php endif; ?> 100 152 <?php endif; ?> 101 153 </div> 102 </div> 103 </div> 104 <div class="box-row flex-start"> 105 <div class="box-row-title not-label pt-12"> 106 <label for="key">セットアップキー</label> 107 </div> 108 <div class="box-row-content"> 109 <div class="flex"> 110 <input type="text" id="key" name="key" 111 value="<?php echo esc_attr( $this->default_key ); ?>" maxlength="16" 112 readonly/> 113 <button id="generate_key" type="button" class="button button-large"> 114 セットアップキーを生成 154 <button id="generate-recovery" type="button" class="status-area-btn button" <?php echo ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ? 'disabled' : '' ); ?>> 155 <?php echo ( $this->is_registered_recovery ? '再生成' : '生成' ); ?> 115 156 </button> 116 </div>117 <div id="qrcode_container" hidden>118 <div id="qrcode"></div>119 <div class="flex onetime-password-container">120 <label for="google_authenticator_code">認証コード:</label>121 <input type="text" id="google_authenticator_code" name="google_authenticator_code"122 maxlength="6"/>123 </div>124 157 </div> 125 158 </div> … … 127 160 </div> 128 161 </div> 129 <div id="submit-btn-area"> 130 <?php $this->nonce_wp( $this->two_factor_authentication->get_feature_key() ); ?> 131 <p id="guide_message" hidden>アプリケーションに表示された6桁の認証コードを入力し、「変更を保存」ボタンをクリックしてください。</p> 132 <?php $this->submit_button_wp(); ?> 133 </div> 134 </form> 135 <style> 136 #generate_key { 137 margin-left: 16px; 138 } 139 140 #qrcode { 141 margin: 32px 32px 16px; 142 } 143 144 .onetime-password-container { 145 margin-left: -20px; 146 } 147 </style> 162 163 <!-- 設定モーダル --> 164 <div id="setting-modal" class="setting-modal" style="display: none;" data-setting-status="" data-regist-status=""> 165 <div class="setting-modal-content"> 166 <div class="setting-modal-header"> 167 <h2 id="setting-modal-title"> 168 <!-- タイトル --> 169 </h2> 170 <span class="dashicons dashicons-no-alt modal-close"></span> 171 </div> 172 <div id="setting-modal-body" class="setting-modal-body"> 173 <!-- テンプレート --> 174 </div> 175 </div> 176 </div> 177 178 <!-- 確認モーダル --> 179 <div id="confirm-modal" class="confirm-modal" style="display: none;" data-confirm-status=""> 180 <div class="confirm-modal-content"> 181 <div class="confirm-modal-body"> 182 <div class="confirm-message-area"> 183 <h2 id="confirm-message-title"> 184 <!-- タイトル --> 185 </h2> 186 <div id="confirm-message-body" class="confirm-message-body"> 187 <!-- 本文 --> 188 </div> 189 </div> 190 <div id="confirm-modal-btn" class="modal-btn-area-end"> 191 <button type="button" id="confirm-modal-cancel" class="button button-gray modal-close style-cancel">キャンセル</button> 192 <button type="button" id="confirm-modal-ok" class="button button-blue modal-next style-next">OK</button> 193 </div> 194 </div> 195 </div> 196 </div> 197 198 <?php $this->nonce_wp( $this->two_factor_authentication->get_feature_key() ); ?> 199 <input type="hidden" id="ajax-url" value="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" /> 200 </div> 201 202 <!-- 認証方法選択テンプレート --> 203 <template id="select-method-modal"> 204 <p class="modal-text">認証方法を選択してください。</p> 205 <div class="auth-method-options"> 206 <div class="auth-method-option"> 207 <input type="radio" class="circle-radio" id="auth_method_app" name="auth_method_select" value="app" checked> 208 <label for="auth_method_app" class="auth-method-label"> 209 <img src="<?php echo esc_url( plugin_dir_url( dirname( __FILE__, 2 ) ) . 'assets/images/icon_mobile.svg?v=1' ); ?>" alt="スマホアイコン" class="auth-method-icon"> 210 <span class="auth-method-text">Google Authenticatorで認証する</span> 211 </label> 212 </div> 213 <div class="auth-method-option"> 214 <input type="radio" class="circle-radio" id="auth_method_email" name="auth_method_select" value="email"> 215 <label for="auth_method_email" class="auth-method-label"> 216 <img src="<?php echo esc_url( plugin_dir_url( dirname( __FILE__, 2 ) ) . 'assets/images/icon_mail.svg?v=1' ); ?>" alt="メールアイコン" class="auth-method-icon"> 217 <span class="auth-method-text">メールアドレスで認証する</span> 218 </label> 219 </div> 220 </div> 221 <div class="modal-btn-area-end"> 222 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button> 223 <button type="button" id="setting-modal-next" class="button button-blue modal-next">次へ</button> 224 </div> 225 </template> 226 227 <!-- Google Authenticator設定テンプレート --> 228 <template id="app-auth-modal"> 229 <div id="message-area" class="info-box blue" style="display: none;"></div> 230 <div id="error-area" class="error-box red" style="display: none;"></div> 231 <p class="modal-text"> 232 <a class="title-block-link" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a> で以下のQRコードを読み込み、表示された認証コードを入力してください。QRコードを読み込めない場合は、セットアップキーを手動で入力してください。 233 </p> 234 <div class="qr-setup-container"> 235 <div id="qrcode_container" class="qr-code-section"> 236 <div id="qrcode"> 237 <!-- QRコード表示エリア --> 238 </div> 239 </div> 240 <div class="setup-key-section"> 241 <div class="generate-key-section" style="text-align: left;"> 242 <p class="modal-text">セットアップキー:<span id="setup_key_display"></span></p> 243 <p class="modal-text" style="color: #2271b1;"><span class="dashicons dashicons-update"></span><a href="#" id="regenerate-key">新しいキーを生成</a></p> 244 </div> 245 <div class="input-code-section"> 246 <label for="verification-code">認証コード(6桁)</label> 247 <input type="text" id="verification-code" name="verification_code" class="verification-code-app" placeholder="例)123456" /> 248 </div> 249 </div> 250 </div> 251 <div class="modal-btn-area-end"> 252 <input type="hidden" id="secret-key" value="" /> 253 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button> 254 <button type="button" id="setting-modal-next" class="button button-blue modal-next">設定を完了</button> 255 </div> 256 </template> 257 258 <!-- メール認証設定テンプレート --> 259 <template id="email-auth-modal"> 260 <div id="message-area" class="info-box blue" style="display: none;"></div> 261 <div id="error-area" class="error-box red" style="display: none;"></div> 262 <p class="modal-text"> 263 ログイン時に登録されているメールアドレスに認証コードを送信しました。<br /> 264 受信した認証コードを入力してください。<br /> 265 メールアドレス: <strong><?php echo esc_html( $this->mail_address ); ?></strong> 266 </p> 267 <div class="input-code-section"> 268 <label for="verification-code">認証コード(6桁)</label> 269 <input type="text" id="verification-code" name="verification_code" class="verification-code-email" placeholder="例)123456" /> 270 </div> 271 <p class="description"> 272 ※メールが届かない場合は、迷惑メールフォルダをご確認ください。<br /> 273 メールが見つからない場合は、<a href="#" id="resend-email-code" class="resend-email-code-inactive">認証コードを再送信</a> できます。<span class="countdown-message"></span> 274 </p> 275 <div class="modal-btn-area-end"> 276 <input type="hidden" id="secret-key" value="" /> 277 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button> 278 <button type="button" id="setting-modal-next" class="button button-blue modal-next">設定を完了</button> 279 </div> 280 </template> 281 282 <!-- リカバリーコードの作成案内テンプレート --> 283 <template id="suggest-recovery-modal"> 284 <div class="status-registered-back-none"> 285 <span class="dashicons dashicons-yes-alt"></span> 286 <p class="h2-text"><strong>2段階認証が有効になりました。</strong></p> 287 </div> 288 <div class="recovery-text-area"> 289 <p class="modal-text">続けて、<strong>リカバリーコードを生成し、安全な場所に保管してください。</strong></p> 290 <p class="modal-text">未生成の場合、デバイス紛失時などにログインできなくなる恐れがあります。</p> 291 </div> 292 <div class="modal-btn-area-end"> 293 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">あとで生成</button> 294 <button type="button" id="setting-modal-next" class="button button-blue modal-next">リカバリーコードを生成</button> 295 </div> 296 </template> 297 298 <!-- リカバリーコードテンプレート(成功) --> 299 <template id="recovery-code-modal-success"> 300 <div class="recovery-modal-body-top"> 301 <div class="recovery-text-area"> 302 <p class="modal-text" style="margin-bottom: 4px;">リカバリーコードを生成しました。</p> 303 <div class="recovery-text"> 304 <div class="black-circle"></div><p class="modal-text">この画面を閉じると<strong>コードは再表示できません</strong>。</p> 305 </div> 306 <div class="recovery-text"> 307 <div class="black-circle"></div><p class="modal-text">各コードは1回のみ使用できます。</p> 308 </div> 309 <div class="recovery-text"> 310 <div class="black-circle"></div><p class="modal-text">必ずコピーまたはダウンロードして安全な場所に保管してください。</p> 311 </div> 312 </div> 313 <div id="recovery-codes-container" class="recovery-codes-grid"> 314 <!-- リカバリーコード表示エリア --> 315 </div> 316 <div class="modal-btn-area-center"> 317 <button type="button" id="recovery-code-copy" class="button button-blue">コピー</button> 318 <button type="button" id="recovery-code-download" class="button button-blue">ダウンロード</button> 319 </div> 320 </div> 321 <div class="recovery-modal-body-bottom"> 322 <div class="modal-btn-area-end"> 323 <button type="button" id="recovery-modal-close" class="button modal-close">閉じる</button> 324 </div> 325 </div> 326 </template> 327 328 <!-- リカバリーコードテンプレート(失敗) --> 329 <template id="recovery-code-modal-failure"> 330 <div class="recovery-modal-body-top"> 331 <p class="modal-text">エラーが発生しました。しばらく待ってから再度お試しください。</p> 332 </div> 333 <div class="recovery-modal-body-bottom"> 334 <div class="modal-btn-area-end"> 335 <button type="button" id="recovery-modal-close" class="button modal-close">閉じる</button> 336 <button type="button" id="recovery-modal-retry" class="button modal-next">再試行</button> 337 </div> 338 </div> 339 </template> 340 148 341 <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" 149 342 type="text/javascript"></script> 150 343 <script type="text/javascript"> 151 function quintetCount(buff) { 152 const quintets = Math.floor(buff.length / 5); 153 return buff.length % 5 === 0 ? quintets : quintets + 1; 154 } 155 156 function bufferToBase32(plain) { 157 let i = 0; 158 let j = 0; 159 let shiftIndex = 0; 160 let digit = 0; 161 const charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 162 const encoded = new Array(quintetCount(plain) * 8); 163 164 /* byte by byte は、quintet by quintet ほどきれいではありませんが、テストは少し速くなります。 再訪する必要があります。 */ 165 while (i < plain.length) { 166 const current = plain[i]; 167 168 if (shiftIndex > 3) { 169 digit = current & (0xff >> shiftIndex); 170 shiftIndex = (shiftIndex + 5) % 8; 171 digit = (digit << shiftIndex) | ((i + 1 < plain.length) ? 172 plain[i + 1] : 0) >> (8 - shiftIndex); 173 i++; 344 const messageRegist = '認証方法の設定が完了しました。'; 345 const messageUpdate = '認証方法の変更が完了しました。'; 346 const messageError = 'エラーが発生しました。しばらく待ってから再度お試しください。'; 347 const messageAppCodeEmpty = '認証に失敗しました。<br />QRコードをGoogle Authenticator でスキャンし、認証コードを入力してください。'; 348 const messageAppCodeInvalid = '認証に失敗しました。コードが正しいか確認し、もう一度お試しください。'; 349 const messageEmailCodeEmpty = '認証に失敗しました。メールで送信された認証コードを入力してください'; 350 const messageEmailCodeInvalid = '認証に失敗しました。認証コードが間違っているか、有効期限が切れています。'; 351 const messageEmailSent = '認証コードを再送信しました。メールをご確認ください。'; 352 const ajaxUrl = document.getElementById('ajax-url').value; 353 354 let emailSentTime = null; 355 let countdownTimeout = null; 356 let qrcodeInstance = null; 357 358 // モーダル内のすべてのボタンを非活性処理 359 function disableModalButtons() { 360 const settingModal = document.getElementById('setting-modal'); 361 const confirmModal = document.getElementById('confirm-modal'); 362 363 if (settingModal && settingModal.style.display === 'block') { 364 const buttons = settingModal.querySelectorAll('button'); 365 buttons.forEach(btn => { 366 btn.disabled = true; 367 }); 368 // ×ボタンも非活性化 369 const closeIcon = settingModal.querySelector('.dashicons.modal-close'); 370 if (closeIcon) { 371 closeIcon.style.pointerEvents = 'none'; 372 closeIcon.style.opacity = '0.5'; 373 } 374 } 375 376 if (confirmModal && confirmModal.style.display === 'block') { 377 const buttons = confirmModal.querySelectorAll('button'); 378 buttons.forEach(btn => { 379 btn.disabled = true; 380 }); 381 // ×ボタンも非活性化 382 const closeIcon = confirmModal.querySelector('.dashicons.modal-close'); 383 if (closeIcon) { 384 closeIcon.style.pointerEvents = 'none'; 385 closeIcon.style.opacity = '0.5'; 386 } 387 } 388 } 389 // モーダル内のすべてのボタンを活性処理 390 function enableModalButtons() { 391 const settingModal = document.getElementById('setting-modal'); 392 const confirmModal = document.getElementById('confirm-modal'); 393 394 if (settingModal && settingModal.style.display === 'block') { 395 const buttons = settingModal.querySelectorAll('button'); 396 buttons.forEach(btn => { 397 btn.disabled = false; 398 }); 399 // ×ボタンも活性化 400 const closeIcon = settingModal.querySelector('.dashicons.modal-close'); 401 if (closeIcon) { 402 closeIcon.style.pointerEvents = 'auto'; 403 closeIcon.style.opacity = '1'; 404 } 405 } 406 407 if (confirmModal && confirmModal.style.display === 'block') { 408 const buttons = confirmModal.querySelectorAll('button'); 409 buttons.forEach(btn => { 410 btn.disabled = false; 411 }); 412 // ×ボタンも活性化 413 const closeIcon = confirmModal.querySelector('.dashicons.modal-close'); 414 if (closeIcon) { 415 closeIcon.style.pointerEvents = 'auto'; 416 closeIcon.style.opacity = '1'; 417 } 418 } 419 } 420 // ページリロード処理 421 function reloadPageWithMessage(message, error) { 422 const form = document.createElement('form'); 423 form.method = 'POST'; 424 form.action = window.location.href; 425 426 const inputMessage = document.createElement('input'); 427 inputMessage.type = 'hidden'; 428 inputMessage.name = 'message'; 429 inputMessage.value = message; 430 431 const inputError = document.createElement('input'); 432 inputError.type = 'hidden'; 433 inputError.name = 'error'; 434 inputError.value = error; 435 436 form.appendChild(inputMessage); 437 form.appendChild(inputError); 438 document.body.appendChild(form); 439 form.submit(); 440 } 441 // モーダル内にメッセージ表示処理 442 function showModalMessage(message) { 443 const modalBody = document.getElementById('setting-modal-body'); 444 const messageArea = document.getElementById('message-area'); 445 const errorArea = document.getElementById('error-area'); 446 if (errorArea) { 447 errorArea.innerHTML = ''; 448 errorArea.style.display = 'none'; 449 } 450 if (messageArea) { 451 messageArea.innerHTML = message; 452 messageArea.style.display = 'block'; 453 } 454 setTimeout(() => { 455 modalBody.scrollTop = 0; 456 }, 0); 457 } 458 // モーダル内にエラー表示処理 459 function showModalError(message) { 460 const modalBody = document.getElementById('setting-modal-body'); 461 const messageArea = document.getElementById('message-area'); 462 const errorArea = document.getElementById('error-area'); 463 if (messageArea) { 464 messageArea.innerHTML = ''; 465 messageArea.style.display = 'none'; 466 } 467 if (errorArea) { 468 errorArea.innerHTML = message; 469 errorArea.style.display = 'block'; 470 } 471 setTimeout(() => { 472 modalBody.scrollTop = 0; 473 }, 0); 474 } 475 // 再送信リンクの有効/無効の制御処理 476 function updateResendLinkState() { 477 const resendLink = document.getElementById('resend-email-code'); 478 const countdownSpan = document.querySelector('.countdown-message'); 479 480 if (!resendLink || !emailSentTime || !countdownSpan) { 481 return; 482 } 483 484 // 現在時刻と送信可能時刻を比較して残り秒数を計算 485 const now = Date.now(); 486 const remaining = Math.ceil((emailSentTime - now) / 1000); 487 488 if (remaining > 0) { 489 // リンクを非活性化してカウントダウン表示 490 resendLink.className = 'resend-email-code-inactive'; 491 countdownSpan.textContent = ` (${remaining}秒後)`; 492 493 // 1秒後に再度更新 494 countdownTimeout = setTimeout(updateResendLinkState, 1000); 495 } else { 496 // リンクを有効化してカウントダウン非表示 497 resendLink.className = 'resend-email-code-active'; 498 countdownSpan.textContent = ''; 499 500 // タイマーを停止(これ以上更新不要) 501 countdownTimeout = null; 502 } 503 } 504 // カウントダウンタイマーを開始処理 505 function startCountdown() { 506 // 既存のタイマーがあればクリア 507 if (countdownTimeout) { 508 clearTimeout(countdownTimeout); 509 countdownTimeout = null; 510 } 511 512 // 即座に状態を更新 513 updateResendLinkState(); 514 } 515 // QRコードを生成・表示処理 516 function displayQRCode(key) { 517 const qrcodeElement = document.getElementById('qrcode'); 518 const setupKeyDisplay = document.getElementById('setup_key_display'); 519 520 if (!qrcodeElement) { 521 console.error('QRコード要素が見つかりません'); 522 return 523 } 524 525 // 既存のQRコードを完全にクリア 526 qrcodeElement.innerHTML = ''; 527 qrcodeInstance = null; 528 529 // 新しいQRCodeインスタンスを作成 530 qrcodeInstance = new QRCode(qrcodeElement, { 531 correctLevel: QRCode.CorrectLevel.M, 532 width: 200, 533 height: 200 534 }); 535 536 // ユーザー名を取得 537 const nameElement = document.querySelector('.display-name'); 538 const name = nameElement ? nameElement.textContent : 'User'; 539 540 // QRコードを生成 541 const otpauthUrl = `otpauth://totp/${encodeURIComponent(name)}?secret=${key}&issuer=${location.hostname}`; 542 qrcodeInstance.makeCode(otpauthUrl); 543 544 // セットアップキーを表示 545 if (setupKeyDisplay) { 546 setupKeyDisplay.textContent = key; 547 } 548 549 // 認証コード入力欄をクリア&フォーカス 550 setTimeout(function() { 551 const codeInput = document.getElementById('verification-code'); 552 if (codeInput) { 553 codeInput.value = ''; 554 codeInput.focus(); 555 } 556 }, 100); 557 } 558 // メール認証コードを送信処理 559 function generateSecretKeyAndsendEmail(resendFlg) { 560 const nonce = document.getElementById('_wpnonce').value; 561 562 if (!resendFlg) { 563 disableModalButtons(); 564 } 565 566 // AJAXでサーバーに送信 567 jQuery.ajax({ 568 url: ajaxUrl, 569 type: 'POST', 570 data: { 571 action: 'cloudsecurewp_generate_key_and_send_email', 572 nonce: nonce, 573 } 574 }) 575 .done(function(response) { 576 if (response.success) { 577 if (resendFlg === false) { 578 openAuthModal('email'); 579 } 580 if (resendFlg && response.data.is_send_email) { 581 showModalMessage(messageEmailSent); 582 } 583 // APIから受け取った残り秒数を使って送信可能時刻を計算 584 const remainingSeconds = parseInt(response.data.remaining_seconds, 10); 585 emailSentTime = Date.now() + (remainingSeconds * 1000); 586 setTimeout(function() { 587 startCountdown(); 588 const codeInput = document.getElementById('verification-code'); 589 if (codeInput) { 590 codeInput.value = ''; 591 codeInput.focus(); 592 } 593 if (!resendFlg) { 594 enableModalButtons(); 595 } 596 }, 100); 174 597 } else { 175 digit = (current >> (8 - (shiftIndex + 5))) & 0x1f; 176 shiftIndex = (shiftIndex + 5) % 8; 177 if (shiftIndex === 0) { 178 i++; 598 reloadPageWithMessage('', messageError); 599 } 600 return; 601 }) 602 .fail(function(xhr) { 603 reloadPageWithMessage('', messageError); 604 return; 605 }); 606 } 607 // 秘密鍵生成処理(サーバー側から取得) 608 function generateSecretKey(reGenerateFlg) { 609 const nonce = document.getElementById('_wpnonce').value; 610 611 disableModalButtons(); 612 613 // AJAXでサーバーから秘密鍵を取得 614 jQuery.ajax({ 615 url: ajaxUrl, 616 type: 'POST', 617 data: { 618 action: 'cloudsecurewp_generate_key', 619 nonce: nonce 620 } 621 }) 622 .done(function(response) { 623 if (response.success) { 624 const hexKey = response.data.hex; 625 const base32Key = response.data.base32; 626 627 // モーダルを表示 628 if (reGenerateFlg === false) { 629 openAuthModal('app'); 179 630 } 180 } 181 182 encoded[j] = charTable[digit]; 183 j++; 184 } 185 186 for (i = j; i < encoded.length; i++) { 187 encoded[i] = '='; 188 } 189 190 return encoded.join(''); 191 } 192 193 function generateKey() { 194 return bufferToBase32(crypto.getRandomValues(new Uint8Array(10))) 195 } 196 197 const qrcode = new QRCode('qrcode', {correctLevel: QRCode.CorrectLevel.M}) 198 199 function showQRCode(key) { 200 document.getElementById('qrcode_container').hidden = false 201 document.getElementById('guide_message').hidden = false 202 const name = document.querySelector('.display-name').textContent 203 204 // https://github.com/google/google-authenticator/wiki/Key-Uri-Format 205 qrcode.makeCode(`otpauth://totp/${encodeURIComponent(name)}?secret=${key}&issuer=${location.hostname}`) 206 207 document.getElementById('google_authenticator_code').focus() 208 } 209 210 // ページ読み込み時に秘密鍵が入力されている場合はQRコードを表示 211 // (認証コードエラー時の再表示のため) 212 const secretKey = document.getElementById('key').value 213 if (secretKey) { 214 showQRCode(secretKey) 215 } 216 217 document.getElementById('generate_key').addEventListener('click', () => { 218 const newKey = generateKey() 219 document.getElementById('key').value = newKey 220 showQRCode(newKey) 631 632 // DOMが構築されるのを待ってから処理を実行 633 setTimeout(function() { 634 const secretKeyEle = document.getElementById('secret-key'); 635 if (secretKeyEle) { 636 secretKeyEle.value = hexKey; 637 } 638 displayQRCode(base32Key); 639 enableModalButtons(); 640 }, 100); 641 } else { 642 reloadPageWithMessage('', messageError); 643 } 644 return; 645 }) 646 .fail(function(xhr) { 647 reloadPageWithMessage('', messageError); 648 return; 649 }); 650 } 651 // 認証コード検証処理 652 function verifyCode(method) { 653 const secretKey = document.getElementById('secret-key').value; 654 const codeInput = document.getElementById('verification-code'); 655 const code = codeInput.value.trim(); 656 const nextBtn = document.getElementById('setting-modal-next'); 657 const authMethodStatus = document.getElementById('auth-method-status').dataset.authMethod; 658 const isRegistered = (authMethodStatus !== '0') ? true : false; 659 const nonce = document.getElementById('_wpnonce').value; 660 661 disableModalButtons(); 662 663 if (!code) { 664 if (method === 'app') { 665 showModalError(messageAppCodeEmpty); 666 } else { 667 showModalError(messageEmailCodeEmpty); 668 } 669 enableModalButtons(); 670 codeInput.value = ''; 671 codeInput.focus(); 672 } else { 673 // AJAXでサーバーに送信 674 jQuery.ajax({ 675 url: ajaxUrl, 676 type: 'POST', 677 data: { 678 action: 'cloudsecurewp_verify_auth_code', 679 nonce: nonce, 680 secret_key: secretKey, 681 code: code, 682 method: method 683 } 684 }) 685 .done(function(response) { 686 if (response.success) { 687 if (response.data.has_recovery) { 688 closeSettingModal(); 689 if (isRegistered) { 690 reloadPageWithMessage(messageUpdate, ''); 691 } else { 692 reloadPageWithMessage(messageRegist, ''); 693 } 694 } else { 695 document.getElementById('setting-modal').dataset.registStatus = isRegistered ? 'update' : 'regist'; 696 openSuggestRecoveryModal(); 697 enableModalButtons(); 698 } 699 return; 700 } else { 701 if (method === 'app') { 702 showModalError(messageAppCodeInvalid); 703 } else { 704 showModalError(messageEmailCodeInvalid); 705 } 706 enableModalButtons(); 707 codeInput.value = ''; 708 codeInput.focus(); 709 return; 710 } 711 }) 712 .fail(function(xhr) { 713 reloadPageWithMessage('', messageError); 714 return; 715 }) 716 } 717 } 718 // リカバリーコード生成処理 719 function generateRecoveryCode(openModalFlg) { 720 const nonce = document.getElementById('_wpnonce').value; 721 722 if (openModalFlg) { 723 disableModalButtons(); 724 } 725 726 // AJAXでサーバーに送信 727 jQuery.ajax({ 728 url: ajaxUrl, 729 type: 'POST', 730 data: { 731 action: 'cloudsecurewp_generate_recovery_codes', 732 nonce: nonce 733 } 734 }) 735 .done(function(response) { 736 if (response.success && Array.isArray(response.data.codes) && response.data.codes.length > 0) { 737 // 成功時のリカバリーコードモーダルを表示 738 closeConfirmModal(); 739 openRecoveryCodesSuccessModal(response.data.codes) 740 enableModalButtons(); 741 return; 742 } else { 743 // 失敗時のリカバリーコードモーダルを表示 744 closeConfirmModal(); 745 openRecoveryCodesFailureModal() 746 enableModalButtons(); 747 return; 748 } 749 }) 750 .fail(function(xhr) { 751 // 失敗時のリカバリーコードモーダルを表示 752 closeConfirmModal(); 753 openRecoveryCodesFailureModal() 754 enableModalButtons(); 755 return; 756 }) 757 } 758 // リカバリーコードのコピー処理 759 function codeCopy() { 760 const codeItems = document.querySelectorAll('.recovery-code-item'); 761 const codes = Array.from(codeItems).map(item => item.textContent); 762 const codesText = codes.join('\n'); 763 764 // HTTPS環境またはlocalhost: Clipboard APIを使用 765 if (navigator.clipboard && navigator.clipboard.writeText) { 766 navigator.clipboard.writeText(codesText).then(function() { 767 alert('クリップボードにコピーしました。'); 768 }).catch(function() { 769 fallbackCopy(codesText); 770 }); 771 } else { 772 // HTTP環境: フォールバック処理 773 fallbackCopy(codesText); 774 } 775 } 776 // HTTP環境用のフォールバックコピー処理 777 function fallbackCopy(text) { 778 const textarea = document.createElement('textarea'); 779 textarea.value = text; 780 textarea.style.position = 'fixed'; 781 textarea.style.top = '0'; 782 textarea.style.left = '0'; 783 textarea.style.opacity = '0'; 784 document.body.appendChild(textarea); 785 textarea.focus(); 786 textarea.select(); 787 788 try { 789 const successful = document.execCommand('copy'); 790 if (successful) { 791 alert('クリップボードにコピーしました。'); 792 } else { 793 alert('コピーに失敗しました。手動でコードをコピーしてください。'); 794 } 795 } catch (err) { 796 alert('コピーに失敗しました。手動でコードをコピーしてください。'); 797 } finally { 798 document.body.removeChild(textarea); 799 } 800 } 801 // リカバリーコードのダウンロード処理 802 function codeDownload() { 803 const codeItems = document.querySelectorAll('.recovery-code-item'); 804 const codes = Array.from(codeItems).map(item => item.textContent); 805 const codesText = codes.join('\n'); 806 const blob = new Blob([codesText], { type: 'text/plain' }); 807 const url = URL.createObjectURL(blob); 808 const a = document.createElement('a'); 809 a.href = url; 810 a.download = 'recovery-codes.txt'; 811 a.style.display = 'none'; 812 document.body.appendChild(a); 813 a.click(); 814 815 setTimeout(function() { 816 document.body.removeChild(a); 817 URL.revokeObjectURL(url); 818 }, 100); 819 } 820 // 選択モーダル表示処理 821 function openSelectModal() { 822 const settingModal = document.getElementById('setting-modal'); 823 const targetTitle = document.getElementById('setting-modal-title'); 824 const targetBody = document.getElementById('setting-modal-body'); 825 const template = document.getElementById('select-method-modal'); 826 827 if (template) { 828 settingModal.dataset.settingStatus = 'select_method'; 829 830 targetTitle.textContent = '認証方法の設定'; 831 targetBody.innerHTML = ''; 832 833 const clone = document.importNode(template.content, true); 834 targetBody.appendChild(clone); 835 836 settingModal.style.display = 'block'; 837 document.body.style.overflowY = 'hidden'; 838 } 839 } 840 // 認証モーダル表示処理 841 function openAuthModal(method) { 842 const settingModal = document.getElementById('setting-modal'); 843 const settingTitle = document.getElementById('setting-modal-title'); 844 const settingBody = document.getElementById('setting-modal-body'); 845 let template = null; 846 847 settingModal.dataset.settingStatus = method; 848 849 if (method === 'app') { 850 template = document.getElementById('app-auth-modal'); 851 } else if (method === 'email') { 852 template = document.getElementById('email-auth-modal'); 853 } 854 855 settingBody.innerHTML = ''; 856 857 const clone = document.importNode(template.content, true); 858 settingBody.appendChild(clone); 859 } 860 // リカバリーコード生成提案モーダル表示処理 861 function openSuggestRecoveryModal() { 862 const settingModal = document.getElementById('setting-modal'); 863 const targetTitle = document.getElementById('setting-modal-title'); 864 const targetBody = document.getElementById('setting-modal-body'); 865 const template = document.getElementById('suggest-recovery-modal'); 866 const clone = document.importNode(template.content, true); 867 settingModal.dataset.settingStatus = 'suggest_recovery'; 868 targetBody.innerHTML = ''; 869 targetBody.appendChild(clone); 870 } 871 // 成功時リカバリーコードモーダル表示処理 872 function openRecoveryCodesSuccessModal(codes) { 873 const settingModal = document.getElementById('setting-modal'); 874 const settingTitle = document.getElementById('setting-modal-title'); 875 const settingBody = document.getElementById('setting-modal-body'); 876 877 settingModal.dataset.settingStatus = 'recovery_codes'; 878 settingTitle.textContent = 'リカバリーコード'; 879 settingBody.innerHTML = ''; 880 881 const template = document.getElementById('recovery-code-modal-success'); 882 const clone = document.importNode(template.content, true); 883 settingBody.appendChild(clone); 884 885 // リカバリーコードを表示 886 const container = document.getElementById('recovery-codes-container'); 887 codes.forEach(function(code) { 888 const codeItem = document.createElement('div'); 889 codeItem.className = 'recovery-code-item'; 890 codeItem.textContent = code; 891 container.appendChild(codeItem); 892 }) 893 894 settingBody.className = 'recovery-modal-body'; 895 settingModal.style.display = 'block'; 896 } 897 // 失敗時リカバリーコードモーダル表示処理 898 function openRecoveryCodesFailureModal() { 899 const settingModal = document.getElementById('setting-modal'); 900 const settingTitle = document.getElementById('setting-modal-title'); 901 const settingBody = document.getElementById('setting-modal-body'); 902 903 settingModal.dataset.settingStatus = 'recovery_codes'; 904 settingTitle.textContent = 'リカバリーコードの生成に失敗しました'; 905 settingBody.innerHTML = ''; 906 907 const template = document.getElementById('recovery-code-modal-failure'); 908 const clone = document.importNode(template.content, true); 909 settingBody.appendChild(clone); 910 911 settingBody.className = 'recovery-modal-body'; 912 settingModal.style.display = 'block'; 913 } 914 // 確認モーダル表示処理 915 function openConfirmModal(status) { 916 const contentBody = document.getElementById('content-body'); 917 const modal = document.getElementById('confirm-modal'); 918 const title = document.getElementById('confirm-message-title'); 919 const modalBody = document.getElementById('confirm-message-body'); 920 921 if (status === 'recovery') { 922 title.textContent = 'リカバリーコードを再生成しますか?'; 923 modalBody.innerHTML = ` 924 <div class="recovery-text"> 925 <div class="black-circle"></div><p class="modal-text">誰にも見られていない安全な環境で実行してください。</p> 926 </div> 927 <div class="recovery-text"> 928 <div class="black-circle"></div><p class="modal-text">再生成を行うと既存のリカバリーコードはすべて使用できなくなります。</p> 929 </div> 930 `; 931 document.body.style.overflowY = 'hidden'; 932 } else { 933 title.textContent = '処理を中断しますか?'; 934 modalBody.innerHTML = '<p class="modal-text">保存していない変更は失われます。</p>'; 935 } 936 937 modal.dataset.confirmStatus = status; 938 modal.style.display = 'block'; 939 } 940 // 設定モーダル非表示処理 941 function closeSettingModal() { 942 const settingModal = document.getElementById('setting-modal'); 943 const settingTitle = document.getElementById('setting-modal-title'); 944 const settingBody = document.getElementById('setting-modal-body'); 945 946 settingTitle.innerHTML = ''; 947 settingBody.innerHTML = ''; 948 settingBody.className = 'setting-modal-body'; 949 settingModal.style.display = 'none'; 950 } 951 // 確認モーダル非表示処理 952 function closeConfirmModal() { 953 const confirmModal = document.getElementById('confirm-modal'); 954 const confirmTitle = document.getElementById('confirm-message-title'); 955 const confirmBody = document.getElementById('confirm-message-body'); 956 957 confirmTitle.innerHTML = ''; 958 confirmBody.innerHTML = ''; 959 confirmModal.style.display = 'none'; 960 } 961 // 設定モーダルの次へボタン押下時のアクション 962 function handleSettingNext(status) { 963 if (status === 'select_method') { 964 // 次へボタン押下時の処理 965 const selectedMethod = document.querySelector('input[name="auth_method_select"]:checked').value; 966 if (selectedMethod === 'app') { 967 // アプリ認証の場合はQRコード生成処理を実行 968 generateSecretKey(false); 969 } else { 970 generateSecretKeyAndsendEmail(false); 971 } 972 } else if (status === 'app' || status === 'email') { 973 // 設定を完了ボタン押下時の処理 974 verifyCode(status); 975 } else if (status === 'suggest_recovery' || status === 'recovery_codes') { 976 // リカバリーコードを生成ボタン押下時の処理 977 generateRecoveryCode(true); 978 } 979 } 980 // 確認モーダルの次へボタン押下時のアクション 981 function handleConfirmNext(status) { 982 if (status === 'recovery') { 983 generateRecoveryCode(true); 984 } else { 985 if (countdownTimeout) { 986 clearTimeout(countdownTimeout); 987 countdownTimeout = null; 988 } 989 closeConfirmModal(); 990 closeSettingModal(); 991 document.body.style.overflowY = 'auto'; 992 } 993 } 994 // 設定モーダルの閉じるボタン押下時のアクション 995 function handleSettingClose(status) { 996 if (status === 'select_method') { 997 closeSettingModal(); 998 document.body.style.overflowY = 'auto'; 999 } else if (status === 'suggest_recovery' || status === 'recovery_codes') { 1000 const registStatus = document.getElementById('setting-modal').dataset.registStatus; 1001 if (registStatus === 'regist') { 1002 reloadPageWithMessage(messageRegist, ''); 1003 } else if (registStatus === 'update') { 1004 reloadPageWithMessage(messageUpdate, ''); 1005 } else { 1006 reloadPageWithMessage('', ''); 1007 } 1008 } else { 1009 openConfirmModal(status); 1010 } 1011 } 1012 // リカバリーコード生成ボタン押下時のアクション 1013 function handleCreateRecovery() { 1014 // リカバリーコードの生成状態を確認 1015 const registerMethodBtn = document.getElementById('register-method'); 1016 const generateRecoveryBtn = document.getElementById('generate-recovery'); 1017 const isRegisteredRecovery = document.getElementById('recovery-code-status').dataset.isRegisteredRecovery; 1018 if (isRegisteredRecovery === '1') { 1019 // 既にリカバリーコードが登録されている場合は確認モーダルを表示 1020 openConfirmModal('recovery'); 1021 } else { 1022 // リカバリーコードが未登録の場合は直接生成処理を実行 1023 registerMethodBtn.disabled = true; 1024 generateRecoveryBtn.disabled = true; 1025 generateRecoveryCode(false); 1026 } 1027 } 1028 1029 // 画面読み込み時の処理 1030 document.addEventListener('DOMContentLoaded', function() { 1031 const registerMethodBtn = document.getElementById('register-method'); 1032 const generateRecoveryBtn = document.getElementById('generate-recovery'); 1033 const settingModal = document.getElementById('setting-modal'); 1034 const confirmModal = document.getElementById('confirm-modal'); 1035 1036 // 認証方法「設定」ボタンのイベント付与 1037 if (registerMethodBtn) { 1038 registerMethodBtn.addEventListener('click', function() { 1039 openSelectModal(); 1040 }) 1041 } 1042 // リカバリーコード「生成」ボタンのイベント付与 1043 if (generateRecoveryBtn) { 1044 generateRecoveryBtn.addEventListener('click', function() { 1045 handleCreateRecovery(); 1046 }) 1047 } 1048 // 設定モーダル内のボタンイベント付与 1049 if (settingModal) { 1050 settingModal.addEventListener('click', (e) => { 1051 const nextSettingBtn = e.target.closest('.modal-next'); 1052 const closeSettingBtn = e.target.closest('.modal-close'); 1053 const regenerateKeyBtn = e.target.closest('#regenerate-key'); 1054 const resendEmailBtn = e.target.closest('#resend-email-code'); 1055 const codeCopyBtn = e.target.closest('#recovery-code-copy'); 1056 const codeDownloadBtn = e.target.closest('#recovery-code-download'); 1057 // 共通「次へ」ボタンイベント付与 1058 if (nextSettingBtn) { 1059 const settingStatus = settingModal.dataset.settingStatus; 1060 handleSettingNext(settingStatus); 1061 } 1062 // 共通「キャンセル」ボタン「×」ボタンイベント付与 1063 if (closeSettingBtn) { 1064 const settingStatus = settingModal.dataset.settingStatus; 1065 handleSettingClose(settingStatus); 1066 } 1067 // シークレットキー再生成リンクイベント付与 1068 if (regenerateKeyBtn) { 1069 generateSecretKey(true); 1070 } 1071 // メール認証コード再送信リンクイベント付与 1072 if (resendEmailBtn) { 1073 generateSecretKeyAndsendEmail(true); 1074 } 1075 // リカバリーコードコピーイベント付与 1076 if (codeCopyBtn) { 1077 codeCopy(); 1078 } 1079 // リカバリーコードダウンロードイベント付与 1080 if (codeDownloadBtn) { 1081 codeDownload(); 1082 } 1083 }); 1084 } 1085 // 確認モーダル内のボタンイベント付与 1086 if (confirmModal) { 1087 confirmModal.addEventListener('click', (e) => { 1088 const nextConfirmBtn = e.target.closest('.modal-next'); 1089 const closeConfirmBtn = e.target.closest('.modal-close'); 1090 1091 // 共通「次へ」ボタンイベント付与 1092 if (nextConfirmBtn) { 1093 const confirmStatus = confirmModal.dataset.confirmStatus; 1094 handleConfirmNext(confirmStatus); 1095 } 1096 // 共通「キャンセル」ボタン「×」ボタンイベント付与 1097 if (closeConfirmBtn) { 1098 closeConfirmModal(); 1099 } 1100 }) 1101 } 221 1102 }) 222 1103 </script> 1104 223 1105 <?php 224 1106 } -
cloudsecure-wp-security/trunk/modules/admin/two-factor-authentication.php
r3124889 r3462167 51 51 52 52 $this->two_factor_authentication->save_settings( $this->datas ); 53 54 53 } 55 54 } … … 75 74 <div class="title-bottom-text"> 76 75 ユーザー名とパスワードの入力に加え、別のコードで追加認証を行います。<br /> 77 <b>※利用するには、<a class="title-block-link" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a> アプリケーションでデバイスを登録する必要があります。</b> 76 2段階認証機能を利用するには、各ユーザーが自身で認証方法の設定を行う必要があります。<br /> 77 <?php if ( 't' === $this->datas['two_factor_authentication'] ) : ?> 78 <b>※認証方法が未設定の場合は、<a href="<?php echo esc_url( admin_url( 'admin.php?page=cloudsecurewp_two_factor_authentication_registration' ) ); ?>">認証方法の設定</a>を行ってください。</b><br /> 79 <?php endif; ?> 78 80 </div> 79 81 <?php … … 111 113 value="<?php echo esc_attr( $role ); ?>"<?php checked( in_array( $role, $roles ) ); ?> /> 112 114 <label for="<?php echo esc_attr( $role ); ?>"><?php echo esc_html_x( ucfirst( $role ), 'User role' ); ?></label> 113 <?php if ( $key !== $lastkey ) : ?> 114 <br/> 115 <?php endif; ?> 115 <br/> 116 116 <?php endforeach; ?> 117 <p class="description"> 118 ユーザーごとの設定状況(未設定/設定済)は <a href="<?php echo esc_url( admin_url( 'users.php' ) ); ?>">ユーザー一覧</a> 画面で確認してください。 119 </p> 117 120 </div> 118 121 </div> -
cloudsecure-wp-security/trunk/modules/cloudsecure-wp.php
r3444508 r3462167 20 20 require_once __DIR__ . '/../really-simple-captcha/really-simple-captcha.php'; 21 21 require_once __DIR__ . '/lib/class-time-based-one-time-password.php'; 22 require_once __DIR__ . '/lib/class-recovery-codes.php'; 22 23 require_once __DIR__ . '/login-log.php'; 23 24 require_once __DIR__ . '/two-factor-authentication.php'; … … 141 142 142 143 if ( $this->disable_access_system_file->is_enabled() ) { 143 add_action( 'plugins_loaded', array( $this->disable_access_system_file, 'init' ), 1 0);144 add_action( 'plugins_loaded', array( $this->disable_access_system_file, 'init' ), 11 ); 144 145 } 145 146 … … 258 259 } 259 260 261 if ( $this->two_factor_authentication->is_enabled() ) { 262 // 2段階認証のAJAXハンドラー 263 add_action( 'wp_ajax_cloudsecurewp_generate_key', array( $this->two_factor_authentication, 'ajax_generate_key' ) ); 264 add_action( 'wp_ajax_cloudsecurewp_generate_key_and_send_email', array( $this->two_factor_authentication, 'ajax_generate_key_and_send_email' ) ); 265 add_action( 'wp_ajax_cloudsecurewp_verify_auth_code', array( $this->two_factor_authentication, 'ajax_verify_auth_code' ) ); 266 add_action( 'wp_ajax_cloudsecurewp_generate_recovery_codes', array( $this->two_factor_authentication, 'ajax_generate_recovery_codes' ) ); 267 } 268 260 269 if ( $this->two_factor_authentication->is_enabled() && 'xmlrpc.php' !== basename( $_SERVER['SCRIPT_NAME'] ) && ! is_admin() ) { 261 270 add_filter( 'sanitize_user', array( $this->two_factor_authentication, 'restore_login_name' ), 0, 1 ); 262 271 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 ); 272 add_filter( 'authenticate', array( $this->two_factor_authentication, 'two_factor_disable_login_check' ), 100, 3 ); 273 add_filter( 'authenticate', array( $this->two_factor_authentication, 'authenticate_with_two_factor' ), 101, 3 ); 264 274 add_action( 'wp_login', array( $this->two_factor_authentication, 'redirect_if_not_two_factor_authentication_registered' ), 10, 2 ); 265 275 } … … 356 366 357 367 if ( $this->two_factor_authentication->is_enabled_on_screen() ) { 358 add_menu_page( ' デバイス登録', '2段階認証', 'read', $slug . '_two_factor_authentication_registration', array( $this, 'm_two_factor_authentication_registration' ), 'dashicons-lock', 72 );368 add_menu_page( '2段階認証の設定', '2段階認証の設定', 'read', $slug . '_two_factor_authentication_registration', array( $this, 'm_two_factor_authentication_registration' ), 'dashicons-lock', 72 ); 359 369 } 360 370 } … … 681 691 } 682 692 693 if ( version_compare( $old_version, '1.4.0' ) < 0 ) { 694 $this->two_factor_authentication->setup_2fa_tables(); 695 } 696 683 697 $this->config->set( 'version', $now_version ); 684 698 $this->config->save(); -
cloudsecure-wp-security/trunk/modules/common.php
r3153634 r3462167 31 31 self::LOGIN_STATUS_DISABLED => '無効', 32 32 ); 33 34 protected const METHOD_PAGE = 1; 35 protected const METHOD_XMLRPC = 2; 36 protected const METHODS = array( 37 self::METHOD_PAGE => 'ログインページ', 38 self::METHOD_XMLRPC => 'XML-RPC', 39 ); 33 40 protected $info; 34 41 -
cloudsecure-wp-security/trunk/modules/disable-access-system-file.php
r3186863 r3462167 85 85 $remove_rules = array( 86 86 'ajax_editor' => array( '950005' ), 87 'ajax_customize' => array( '950005' ), 88 'rest_api' => array( '950005' ), 87 89 ); 88 90 -
cloudsecure-wp-security/trunk/modules/lib/class-time-based-one-time-password.php
r3101467 r3462167 6 6 7 7 /** 8 * タイムベースドワンタイムパスワードアルゴリズムの2段階認証のためのクラス8 * TOTPアルゴリズムの2段階認証のためのクラス 9 9 */ 10 10 class CloudSecureWP_Time_Based_One_Time_Password { 11 private static $digits = 6; 11 private static $digits = 6; 12 private static $discrepancy = 1; 12 13 13 14 /** 14 15 * 指定されたシークレットと時点を使用してコードを計算 15 16 * 16 * @param string $secret17 * @param int |null$time_slice17 * @param string $secret 18 * @param int $time_slice 18 19 * 19 20 * @return string 20 21 */ 21 public static function get_code( string $secret, int $time_slice = null ): string { 22 if ( $time_slice === null ) { 23 $time_slice = floor( time() / 30 ); 24 } 25 26 $secret_key = self::base32_decode( $secret ); 22 public static function get_code( string $secret, int $time_slice ): string { 23 // 16進数をバイナリデータに変換 24 $secret_key = hex2bin( $secret ); 27 25 28 26 // 時間をバイナリ文字列にパック … … 48 46 /** 49 47 * コードが正しいかどうかを検証 50 * $discrepancy*30 秒前から今から $discrepancy*30 秒までのコードを受け入れます。 51 * 52 * @param string $secret 53 * @param string $code 54 * @param int $discrepancy 30 秒単位で許容される時間のずれ (8 は前後 4 分を意味します) 55 * @param int|null $current_time_slice 48 * 前後1つ分の時間スライスを許容 49 * 50 * @param string $secret 51 * @param string $code 52 * @param int $time_step 時間間隔(秒)デフォルトは30秒 56 53 * 57 54 * @return bool 58 55 */ 59 public static function verify_code( string $secret, string $code, int $discrepancy = 1, int $current_time_slice = null ): bool { 60 if ( $current_time_slice === null ) { 61 $current_time_slice = floor( time() / 30 ); 62 } 56 public static function verify_code( string $secret, string $code, int $time_step ): bool { 57 $current_time_slice = floor( time() / $time_step ); 63 58 64 59 if ( strlen( $code ) !== 6 ) { … … 66 61 } 67 62 68 for ( $i = - $discrepancy; $i <=$discrepancy; ++$i ) {63 for ( $i = - self::$discrepancy; $i <= self::$discrepancy; ++$i ) { 69 64 $calculated_code = self::get_code( $secret, $current_time_slice + $i ); 70 65 if ( self::timing_safe_equals( $calculated_code, $code ) ) { … … 79 74 * Base32をデコード 80 75 * 81 * @param $secret76 * @param string $secret 82 77 * 83 78 * @return bool|string 84 79 */ 85 p rotectedstatic function base32_decode( $secret ) {80 public static function base32_decode( $secret ) { 86 81 if ( empty( $secret ) ) { 87 82 return ''; … … 151 146 return $result === 0; 152 147 } 148 149 /** 150 * メール認証用のコードを生成 151 * 152 * @param string $secret 153 * @param int $time_step 時間間隔(秒) 154 * 155 * @return string 156 */ 157 public static function create_code_for_email( string $secret, int $time_step ): string { 158 159 // 現在の時間スライスを計算 160 $time_slice = floor( time() / $time_step ); 161 162 // コードを生成 163 return self::get_code( $secret, $time_slice ); 164 } 165 166 /** 167 * Base32エンコード 168 * 169 * @param string $binary バイナリデータ 170 * 171 * @return string 172 */ 173 public static function base32_encode( string $binary ): string { 174 if ( '' === $binary ) { 175 return ''; 176 } 177 178 $base32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 179 $v = 0; 180 $vbits = 0; 181 $ret = ''; 182 183 // 1. 5ビットずつ切り出して変換 184 for ( $i = 0, $j = strlen( $binary ); $i < $j; $i++ ) { 185 $v <<= 8; 186 $v += ord( $binary[ $i ] ); 187 $vbits += 8; 188 189 while ( $vbits >= 5 ) { 190 $vbits -= 5; 191 $ret .= $base32_chars[ ( $v >> $vbits ) & 31 ]; 192 } 193 } 194 195 // 2. 余ったビットの処理 196 if ( $vbits > 0 ) { 197 $v <<= ( 5 - $vbits ); 198 $ret .= $base32_chars[ $v & 31 ]; 199 } 200 201 // 3. RFC 4648 に基づくパディング処理(デコーダーのチェックをパスするために必須) 202 // Base32は8文字(40ビット)単位でブロックを作る必要がある 203 $padding = ( 8 - ( strlen( $ret ) % 8 ) ) % 8; 204 $ret .= str_repeat( '=', $padding ); 205 206 return $ret; 207 } 208 209 /** 210 * ランダムな秘密鍵を生成 211 * 212 * @return array ['hex' => 16進数文字列, 'base32' => Base32文字列] 213 */ 214 public static function generate_secret_key(): array { 215 // ランダムなバイナリデータを生成 216 $binary = random_bytes( 10 ); 217 218 // 16進数に変換 219 $hex = bin2hex( $binary ); 220 221 // Base32エンコード 222 $base32 = self::base32_encode( $binary ); 223 224 return array( 225 'hex' => $hex, 226 'base32' => $base32, 227 ); 228 } 153 229 } -
cloudsecure-wp-security/trunk/modules/login-log.php
r3124889 r3462167 24 24 ); 25 25 26 private const METHOD_PAGE = 1;27 private const METHOD_XMLRPC = 2;28 // private const METHOD_RESTAPI = 3;29 private const METHODS = array(30 self::METHOD_PAGE => 'ログインページ',31 self::METHOD_XMLRPC => 'XML-RPC',32 // self::METHOD_RESTAPI => 'REST API',33 );34 26 private const MAX_LOG = 10000; 35 27 private $config; -
cloudsecure-wp-security/trunk/modules/two-factor-authentication.php
r3422588 r3462167 6 6 7 7 class CloudSecureWP_Two_Factor_Authentication extends CloudSecureWP_Common { 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; 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; 13 private const LOGIN_TABLE_NAME = 'cloudsecurewp_2fa_login'; 14 private const AUTH_TABLE_NAME = 'cloudsecurewp_2fa_auth'; 15 private const LATE_LIMIT_TIME_LIST = array( 16 3 => 60, // 3回で1分間 17 6 => 300, // 6回で5分間 18 9 => 600, // 9回で10分間 19 12 => 900, // 12回で15分間 20 ); 21 private const LATE_LIMIT_TIME_MAX = 1200; // 最大20分間 22 public const USER_AUTH_METHOD_NONE = 0; 23 public const USER_AUTH_METHOD_APP = 1; 24 public const USER_AUTH_METHOD_EMAIL = 2; 25 public const USER_AUTH_METHOD_RECOVERY = 3; 26 public const AUTH_APP_INTERVAL = 30; 27 public const AUTH_EMAIL_INTERVAL = 60; 28 public const TWO_FACTOR_SECRET_KEY = 'wp_cloudsecurewp_two_factor_authentication_secret'; 29 public const TWO_FACTOR_EMAIL_SEND = 'wp_cloudsecurewp_two_factor_authentication_email_send'; 30 public const EMAIL_SEND_LIMIT_TIME = 30; 13 31 14 32 private $config; … … 103 121 $settings = $this->get_default(); 104 122 $this->save_settings( $settings ); 123 $this->create_auth_table(); 124 $this->create_login_table(); 105 125 } 106 126 … … 143 163 144 164 /** 165 * 2段階認証のメッセージを出力 166 * 167 * @param string $message メッセージ 168 * 169 * @return void 170 */ 171 private function login_message( string $message ) { 172 if ( empty( $message ) ) { 173 return; 174 } 175 echo '<div class="notice notice-success">' . esc_html( apply_filters( 'login_messages', $message ) ) . "</div>\n"; 176 } 177 178 /** 145 179 * 2段階認証のエラーを出力 146 180 * 147 * @return void 148 */ 149 private function login_error() { 150 if ( array_key_exists( 'google_authenticator_code', $_REQUEST ) ) { 151 if ( sanitize_text_field( $_REQUEST['google_authenticator_code'] ) ) { 152 $errors = '認証コードが間違っているか、有効期限が切れています。'; 153 } else { 154 $errors = '認証コードが入力されていません。'; 155 } 156 echo '<div id="login_error">' . esc_html( apply_filters( 'login_errors', $errors ) ) . "</div>\n"; 157 } 181 * @param string $error エラーメッセージ 182 * 183 * @return void 184 */ 185 private function login_error( string $error ) { 186 if ( empty( $error ) ) { 187 return; 188 } 189 echo '<div id="login_error" class="notice notice-error">' . esc_html( apply_filters( 'login_errors', $error ) ) . "</div>\n"; 190 } 191 192 /** 193 * 画面表示用のメールアドレスを生成 194 * 195 * @param int $user_id ユーザーID 196 * 197 * @return string 198 */ 199 public function mask_email( int $user_id ): string { 200 $email = get_userdata( $user_id )->user_email; 201 202 list( $user, $full_domain ) = explode( '@', $email ); 203 204 $last_dot_pos = strrpos( $full_domain, '.' ); 205 $domain_name = substr( $full_domain, 0, $last_dot_pos ); 206 $tld = substr( $full_domain, $last_dot_pos ); 207 208 [ $masked_user, $masked_domain ] = array_map( 209 function( $str ) { 210 $showVisible = ( mb_strlen( $str ) > 2 ) ? 2 : 1; 211 return mb_substr( $str, 0, $showVisible ) . '*****'; 212 }, 213 [ $user, $domain_name ] 214 ); 215 216 $masked_address = $masked_user . '@' . $masked_domain . $tld; 217 218 return $masked_address; 158 219 } 159 220 … … 162 223 * 163 224 * @param string $login_token 164 * 165 * @return void 166 */ 167 private function login_form( $login_token ) { 225 * @param int $auth_method 226 * @param bool $has_recovery 227 * @param string $email_address 228 * @param int $remaining_seconds 229 * 230 * @return void 231 */ 232 private function login_form( string $login_token, int $auth_method, bool $has_recovery, string $email_address, int $remaining_seconds ) { 168 233 ?> 169 234 <form name="loginform" id="loginform" 170 235 action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post"> 171 <?php if ( array_key_exists( 'rememberme', $_REQUEST ) && 'forever' === sanitize_text_field( $_REQUEST['rememberme'] ) ) : ?>172 <input name="rememberme" type="hidden" id="rememberme" value="forever"/>173 <?php endif; ?>174 <input type="hidden" name="login_token" value="<?php echo esc_attr( $login_token ); ?>">175 <p>176 <label for="google_authenticator_code">認証コード</label>177 <input type="text" name="google_authenticator_code" id="google_authenticator_code" class="input"178 value="" size="20" autocomplete="one-time-code"/>179 </p>180 <script type="text/javascript">document.getElementById("google_authenticator_code").focus();</script>181 <p>デバイスのGoogle Authenticator アプリケーションに表示されている6桁の認証コードを入力してください。</p>182 <p class="submit">183 236 <?php wp_nonce_field( $this->get_feature_key() . '_csrf' ); ?> 184 <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" 185 value="<?php esc_attr_e( 'Log In' ); ?>"/> 237 <div class="two-fa-form"> 238 <?php if ( array_key_exists( 'rememberme', $_REQUEST ) && 'forever' === sanitize_text_field( $_REQUEST['rememberme'] ) ) : ?> 239 <input name="rememberme" type="hidden" id="rememberme" value="forever"/> 240 <?php endif; ?> 241 <input type="hidden" name="login_token" value="<?php echo esc_attr( $login_token ); ?>"> 186 242 <input type="hidden" name="redirect_to" 187 243 value="<?php echo esc_attr( sanitize_text_field( $_REQUEST['redirect_to'] ?? admin_url() ) ); ?>"/> 188 244 <input type="hidden" name="testcookie" value="1"/> 189 </p> 245 <?php if ( $auth_method === self::USER_AUTH_METHOD_RECOVERY ) : ?> 246 <!-- リカバリーコード入力フォーム --> 247 <input type="hidden" name="recovery_code" value="1"> 248 <div class="text-area"> 249 <p>バックアップとして保存したリカバリーコードを入力してください。</p> 250 </div> 251 <div class="input-area"> 252 <p> 253 <label for="authenticator_code">リカバリーコード</label> 254 <input type="text" name="authenticator_code" id="authenticator_code" class="input two-fa-input" 255 value="" size="20" autocomplete="off"/> 256 </p> 257 </div> 258 <script type="text/javascript">document.getElementById("authenticator_code").focus();</script> 259 <?php else : ?> 260 <!-- 通常の認証コード入力フォーム --> 261 <div class="two-fa-text-area"> 262 <?php if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) : ?> 263 <p><strong><?php echo esc_html( $email_address ); ?></strong>に送信された認証コードを入力してください。</p> 264 <p>※メールが届かない場合、コードを再送信してください。</p> 265 <?php else : ?> 266 <p>デバイスのGoogle Authenticator に表示されている認証コードを入力してください。</p> 267 <?php endif; ?> 268 </div> 269 <div class="two-fa-input-area"> 270 <p> 271 <label for="authenticator_code">認証コード(6桁)</label> 272 <input type="text" name="authenticator_code" id="authenticator_code" class="input two-fa-input" 273 value="" size="20" autocomplete="one-time-code"/> 274 </p> 275 </div> 276 <script type="text/javascript">document.getElementById("authenticator_code").focus();</script> 277 <?php endif; ?> 278 <div> 279 <div class="two-fa-btn-area"> 280 <button type="submit" name="wp-submit" id="wp-submit" class="button button-primary two-fa-btn" style="order: 2;"> 281 <?php esc_attr_e( 'Log In' ); ?> 282 </button> 283 <?php if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) : ?> 284 <button type="submit" name="resend_2fa_email" value="1" id="resend_2fa_email_btn" class="button two-fa-btn" data-cooldown="30" data-remaining_seconds="<?php echo esc_attr( $remaining_seconds ); ?>" style="order: 1;"> 285 再送信 286 </button> 287 <?php endif; ?> 288 </div> 289 <?php if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) : ?> 290 <div id="resend_cooldown_message" class="two-fa-cooldown-message" style="display: none;"></div> 291 <?php endif; ?> 292 </div> 293 <div class="two-fa-link-area"> 294 <?php if ( $auth_method === self::USER_AUTH_METHOD_RECOVERY ) : ?> 295 <!-- 通常の認証コード入力に戻るボタン --> 296 <button type="submit" name="back_to_auth_code" value="1" class="two-fa-link"> 297 ← 認証コード入力に戻る 298 </button> 299 <?php elseif ( $has_recovery ) : ?> 300 <!-- リカバリーコード入力へのボタン --> 301 <div class="separator"> 302 または 303 </div> 304 <button type="submit" name="use_recovery_code" value="1" class="two-fa-link"> 305 リカバリーコードを使用する 306 </button> 307 <?php endif; ?> 308 </div> 309 </div> 190 310 </form> 311 <style> 312 .two-fa-form { 313 display: flex; 314 flex-direction: column; 315 gap: 24px; 316 } 317 .two-fa-text-area { 318 display: flex; 319 flex-direction: column; 320 gap: 3px; 321 } 322 .two-fa-input { 323 margin: 0 !important; 324 } 325 .two-fa-btn-area { 326 display: flex; 327 gap: 16px; 328 justify-content: center; 329 align-items: center; 330 } 331 .two-fa-btn { 332 padding: 0 !important; 333 margin: 0 !important; 334 width: 50%; 335 height: 36px; 336 } 337 .two-fa-link-area { 338 display: flex; 339 flex-direction: column; 340 gap: 16px; 341 text-align: center; 342 } 343 .two-fa-link { 344 padding: 0; 345 border: none; 346 background: none; 347 color: #2271b1; 348 text-decoration: underline; 349 cursor: pointer; 350 } 351 .separator { 352 display: flex; 353 align-items: center; 354 color: #646970; 355 font-size: 14px; 356 } 357 .separator::before, 358 .separator::after { 359 content: ""; 360 flex: 1; 361 height: 1px; 362 background: #B2B2B2; 363 } 364 .separator::before { 365 margin-right: 8px; 366 } 367 368 .separator::after { 369 margin-left: 8px; 370 } 371 .two-fa-cooldown-message { 372 font-size: 12px; 373 text-align: left; 374 color: #646970; 375 } 376 </style> 377 <script type="text/javascript"> 378 (function() { 379 let emailAbleSendTime = null; 380 381 const resendBtn = document.getElementById('resend_2fa_email_btn'); 382 const cooldownMessage = document.getElementById('resend_cooldown_message'); 383 if (!resendBtn || !cooldownMessage) return; 384 385 // サーバーから受け取った残り秒数を使って送信可能時刻を計算 386 const remainingSeconds = parseInt(resendBtn.getAttribute('data-remaining_seconds') || '0', 10); 387 emailAbleSendTime = Date.now() + (remainingSeconds * 1000); 388 389 function updateTimerDisplay() { 390 // 現在時刻と送信可能時刻を比較して残り秒数を計算 391 const now = Date.now(); 392 const remainingTime = Math.ceil((emailAbleSendTime - now) / 1000); 393 394 if (remainingTime > 0) { 395 resendBtn.disabled = true; 396 cooldownMessage.style.display = 'block'; 397 cooldownMessage.textContent = remainingTime + '秒後 再送信できます'; 398 setTimeout(updateTimerDisplay, 1000); 399 } else { 400 resendBtn.disabled = false; 401 cooldownMessage.style.display = 'none'; 402 } 403 } 404 405 // 表示時にタイマーを開始 406 <?php if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) : ?> 407 updateTimerDisplay(); 408 <?php endif; ?> 409 })(); 410 </script> 191 411 <?php 192 412 } … … 202 422 */ 203 423 public function redirect_if_not_two_factor_authentication_registered( $user_login, $user ) { 204 $secret = get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user->ID ); 424 $auth_info = $this->get_2fa_auth_info( $user->ID ); 425 $secret = ( count( $auth_info ) > 0 && isset( $auth_info['secret'] ) ) ? true : false; 205 426 206 427 if ( isset( $user->roles[0] ) ) { … … 234 455 public function show_2factor_state_2user_list( $value, $column_name, $user_id ) { 235 456 if ( $column_name === 'is_2factor' ) { 236 $value = get_user_meta( $user_id, 'wp_cloudsecurewp_two_factor_authentication_secret', true ); 237 return $value !== '' ? '設定済' : '未設定'; 457 $auth_info = $this->get_2fa_auth_info( $user_id ); 458 if ( count( $auth_info ) === 0 ) { 459 // データ移行漏れ対応 460 $auth_info = $this->repair_migration_gaps( $user_id ); 461 } 462 $value = '未設定'; 463 if ( count ( $auth_info ) > 0 ) { 464 $value = '設定済'; 465 } 466 return $value; 238 467 } 239 468 return $value; 240 }241 242 /**243 * ユーザの2faシークレットキー取得244 *245 * @param int $user_id246 *247 * @return mixed248 */249 private function get_2fa_secret_key( int $user_id ) {250 return get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user_id );251 469 } 252 470 … … 329 547 } 330 548 331 // 2faシークレットキーが存在しない場合332 if ( ! $this->get_2fa_secret_key( $user->ID ) ) {333 return false;334 }335 336 549 return true; 337 550 } … … 341 554 * 342 555 * @param string $login_token 343 * 344 * @return void 345 */ 346 private function show_two_factor_form( string $login_token ) { 556 * @param int $auth_method 557 * @param bool $has_recovery 558 * @param string $email_address 559 * @param bool $is_send_email 560 * 561 * @return void 562 */ 563 private function show_two_factor_form( string $login_token, int $auth_method, bool $has_recovery, string $email_address, bool $is_send_email ): void { 564 $message = ''; 565 $error = ''; 566 $remaining_seconds = 0; 567 568 // 再送信メッセージ 569 if ( array_key_exists( 'resend_2fa_email', $_REQUEST ) ) { 570 if ( $is_send_email ) { 571 $message = '認証コードを再送信しました。'; 572 } else { 573 $error = '認証コードの再送信は30秒に1回までです。しばらく時間をおいてから再度お試しください。'; 574 } 575 } 576 577 // エラーメッセージ 578 if ( array_key_exists( 'wp-submit', $_REQUEST ) && array_key_exists( 'authenticator_code', $_REQUEST ) ) { 579 if ( sanitize_text_field( $_REQUEST['authenticator_code'] ) ) { 580 $error = '認証コードが間違っているか、有効期限が切れています。'; 581 } else { 582 $error = '認証コードが入力されていません。'; 583 } 584 } 585 586 if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) { 587 // メール認証の場合、送信可能になるまでの残り秒数を計算 588 $able_send_time = $this->get_email_able_send_time( $this->get_option_data( $this->create_option_key( $login_token ) )['user_id'] ); 589 $remaining_seconds = max( 0, $able_send_time - time() ); 590 } 591 347 592 // 2FA画面を表示 348 593 login_header( '2段階認証画面' ); 349 $this->login_error(); 350 $this->login_form( $login_token ); 594 $this->login_message( $message ); 595 $this->login_error( $error ); 596 $this->login_form( $login_token, $auth_method, $has_recovery, $email_address, $remaining_seconds ); 351 597 login_footer(); 352 598 exit; … … 354 600 355 601 /** 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 ); 602 * メール認証コードの再送信処理 603 * 604 * @param int $user_id 605 * @param string $secret 606 * 607 * @return bool true: 送信成功、false: 送信制限中 608 */ 609 private function send_2fa_email( int $user_id, string $secret = '' ): bool { 610 // 送信制限チェック(30秒間) 611 $current_time = time(); 612 $able_sent_time = $this->get_email_able_send_time( $user_id ); 613 614 if ( $current_time < $able_sent_time ) { 615 // 30秒以内の再送信は処理しない(エラーは表示しない) 616 return false; 617 } 618 619 // シークレットキーを取得 620 if ( $secret === '' ) { 621 $auth_info = $this->get_2fa_auth_info( $user_id ); 622 $secret = $auth_info['secret']; 623 } 624 // 新しいコードを生成して送信 625 $code = CloudSecureWP_Time_Based_One_Time_Password::create_code_for_email( $secret, self::AUTH_EMAIL_INTERVAL ); 626 $this->send_code( $user_id, $code, self::AUTH_EMAIL_INTERVAL, 'login' ); 627 628 // 最終送信時刻を更新 629 $this->update_email_able_send_time( $user_id ); 630 631 return true; 365 632 } 366 633 … … 370 637 * @param int $user_id 371 638 * @param string $code 639 * @param int $auth_method 372 640 * 373 641 * @return bool 374 642 */ 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; 643 private function verify_2fa_code( int $user_id, string $code, int $auth_method ): bool { 644 // リカバリーコードでの認証の場合 645 if ( $auth_method === self::USER_AUTH_METHOD_RECOVERY ) { 646 return CloudSecureWP_Recovery_Codes::verify_code( $user_id, $code ); 647 } 648 649 // シークレットキーを取得 650 $auth_info = $this->get_2fa_auth_info( $user_id ); 651 if ( ! $auth_info || ! isset( $auth_info['secret'] ) ) { 652 return false; 653 } 654 $secret_key = $auth_info['secret']; 655 656 // メール認証の場合(60秒間隔) 657 if ( $auth_method === self::USER_AUTH_METHOD_EMAIL ) { 658 return CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret_key, $code, self::AUTH_EMAIL_INTERVAL ); 659 } 660 661 // アプリ認証の場合(30秒間隔) 662 return CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret_key, $code, self::AUTH_APP_INTERVAL ); 392 663 } 393 664 … … 403 674 404 675 // 初回アクセス・または初回認証の場合、何もしない 405 if ( ! isset( $_POST[' google_authenticator_code'] ) ) {676 if ( ! isset( $_POST['authenticator_code'] ) ) { 406 677 return $username; 407 678 } 408 679 409 // 2FA関連のPOSTデータ取得410 $ post_data = $this->get_2fa_post_data();680 // ログイントークンを取得 681 $login_token = sanitize_text_field( $_POST['login_token'] ?? '' ); 411 682 412 683 // ログイン情報を取得 413 $option_key = $this->create_option_key( $ post_data['login_token']);684 $option_key = $this->create_option_key( $login_token ); 414 685 $option_data = $this->get_option_data( $option_key ); 415 686 … … 433 704 */ 434 705 public function restore_login_session( $user, $username, $password ) { 435 436 706 // 初回アクセス・または初回認証の場合、何もしない 437 if ( ! isset( $_POST[' google_authenticator_code'] ) ) {707 if ( ! isset( $_POST['authenticator_code'] ) ) { 438 708 return $user; 439 709 } … … 442 712 check_admin_referer( $this->get_feature_key() . '_csrf' ); 443 713 444 // 2FA関連のPOSTデータ取得445 $ post_data = $this->get_2fa_post_data();714 // ログイントークンを取得 715 $login_token = sanitize_text_field( $_POST['login_token'] ?? '' ); 446 716 447 717 // ログイン情報を取得 448 $option_key = $this->create_option_key( $ post_data['login_token']);718 $option_key = $this->create_option_key( $login_token ); 449 719 $option_data = $this->get_option_data( $option_key ); 450 720 … … 462 732 } 463 733 464 // ログイン情報のPOSTデータ復元 465 $_POST['log'] = $option_data['user_login']; 734 // 認証方法の設定 735 $auth_method = intVal( $option_data['auth_method'] ); 736 737 // 認証コード再送信 738 if ( isset( $_POST['resend_2fa_email'] ) ) { 739 // メール再送信 740 $result = $this->send_2fa_email( $option_data['user_id'] ); 741 742 $this->show_two_factor_form( 743 $login_token, 744 $auth_method, 745 $option_data['has_recovery'], 746 $option_data['email_address'], 747 $result 748 ); 749 } 750 751 // リカバリーコード入力画面への切り替え 752 if ( isset( $_POST['use_recovery_code'] ) ) { 753 $this->show_two_factor_form( 754 $login_token, 755 self::USER_AUTH_METHOD_RECOVERY, 756 $option_data['has_recovery'], 757 $option_data['email_address'], 758 false 759 ); 760 } 761 762 // 認証コード入力画面への切り替え 763 if ( isset( $_POST['back_to_auth_code'] ) ) { 764 $this->show_two_factor_form( 765 $login_token, 766 $auth_method, 767 $option_data['has_recovery'], 768 $option_data['email_address'], 769 false 770 ); 771 } 772 773 // リカバリコードでの認証の場合 774 if ( isset( $_POST['recovery_code'] ) ) { 775 $auth_method = self::USER_AUTH_METHOD_RECOVERY; 776 } 777 778 // ログイン情報の復元 779 $_POST['log'] = $option_data['user_login']; 780 $_POST['auth_method'] = $auth_method; 781 $_POST['has_recovery'] = $option_data['has_recovery']; 782 $_POST['email_address'] = $option_data['email_address']; 466 783 467 784 return $user; … … 469 786 470 787 /** 471 * 認証フック: 2段階認証 処理788 * 認証フック: 2段階認証ログイン無効チェック 472 789 * 473 790 * @param mixed $user … … 477 794 * @return mixed 478 795 */ 796 public function two_factor_disable_login_check( $user, $username, $password ) { 797 // レートリミットの無効時間を取得 798 $limit_time = $this->two_factor_rate_limit(); 799 if ( $limit_time > 0 ) { 800 // ログインログに記録 801 $this->write_log( self::LOGIN_STATUS_DISABLED ); 802 803 return new WP_Error( 'empty_username', "失敗回数が上限に達したため、{$limit_time}分間ログインできません。しばらく待ってから再度お試しください。" ); 804 } 805 return $user; 806 } 807 808 809 /** 810 * 認証フック: 2段階認証処理 811 * 812 * @param mixed $user 813 * @param string $username 814 * @param string $password 815 * 816 * @return mixed 817 */ 479 818 public function authenticate_with_two_factor( $user, $username, $password ) { 480 481 819 // 初回アクセス、または初回認証時 482 if ( ! isset( $_POST['google_authenticator_code'] ) ) { 483 820 if ( ! isset( $_POST['authenticator_code'] ) ) { 484 821 // 認証失敗の場合 485 822 if ( is_wp_error( $user ) ) { … … 492 829 } 493 830 831 // 認証情報の取得 832 $auth_info = $this->get_2fa_auth_info( $user->ID ); 833 // 認証情報が存在しない場合 834 if ( count( $auth_info ) === 0 ) { 835 // データ移行漏れ対応 836 $auth_info = $this->repair_migration_gaps( $user->ID ); 837 if ( count ( $auth_info ) === 0 ) { 838 return $user; 839 } 840 } 841 842 // リカバリーコードの有無 843 $has_recovery = true; 844 $recovery_codes = $auth_info['recovery']; 845 if ( ! $recovery_codes || ! is_array( $recovery_codes ) || count( $recovery_codes ) === 0 ) { 846 $has_recovery = false; 847 } 848 494 849 // option key生成 495 850 $session_token = bin2hex( random_bytes( 16 ) ); 496 851 $option_key = $this->create_option_key( $session_token ); 497 852 498 // 保存用 認証データ作成853 // 保存用ログインデータ作成 499 854 $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(), 855 'user_id' => $user->ID, 856 'user_login' => sanitize_text_field( $_POST['log'] ?? '' ), 857 'auth_method' => intVal( $auth_info['method'] ), 858 'expires' => time() + self::SESSION_EXPIRY, 859 'created' => time(), 860 'has_recovery' => $has_recovery, 861 'email_address' => $this->mask_email( $user->ID ), 504 862 ); 505 863 … … 507 865 $this->set_option_data( $option_key, $option_data ); 508 866 867 // メール認証の場合、コードを生成して送信 868 if ( intVal( $auth_info['method'] ) === self::USER_AUTH_METHOD_EMAIL ) { 869 $this->send_2fa_email( $user->ID, $auth_info['secret'] ); 870 } 871 509 872 // 2FA画面を表示して、処理終了 510 $this->show_two_factor_form( $session_token ); 873 $this->show_two_factor_form( 874 $session_token, 875 intVal( $auth_info['method'] ), 876 $has_recovery, 877 $option_data['email_address'], 878 false 879 ); 511 880 } 512 881 513 882 // 2FA関連のPOSTデータ取得 514 $post_data = $this->get_2fa_post_data(); 883 $login_token = sanitize_text_field( $_POST['login_token'] ?? '' ); 884 $auth_code = sanitize_text_field( $_POST['authenticator_code'] ?? '' ); 515 885 516 886 // option key取得 517 $option_key = $this->create_option_key( $ post_data['login_token']);887 $option_key = $this->create_option_key( $login_token ); 518 888 519 889 // $userがWP_Errorの場合は処理をスキップ … … 523 893 524 894 // 2段階認証成功の場合 525 if ( $this->verify_2fa_code( $user->ID, $post_data['google_authenticator_code'] ) ) { 895 if ( $this->verify_2fa_code( $user->ID, $auth_code, $_POST['auth_method'] ) ) { 896 // 失敗回数をリセット 897 $this->reset_fail_count(); 898 // セッションデータを削除 526 899 $this->delete_option_data( $option_key ); 900 // メール認証の送信可能時間をリセット 901 $this->delete_email_able_send_time( $user->ID ); 527 902 return $user; 528 903 } 529 904 530 // ログイン失敗時の処理を実行(ログイン回数・ログインログ) 531 do_action( 'wp_login_failed', $_POST['log'], $user ); 905 // 2段階認証失敗の場合 906 // 失敗回数をインクリメント 907 $this->increment_fail_count(); 908 909 // レートリミットの確認 910 $limit_time = $this->two_factor_rate_limit(); 911 912 $this->write_log( self::LOGIN_STATUS_FAILED ); 913 914 if ( $limit_time > 0 ) { 915 // ステータス無効状態の場合 916 return new WP_Error( 'empty_username', "失敗回数が上限に達したため、{$limit_time}分間ログインできません。しばらく待ってから再度お試しください。" ); 917 } 532 918 533 919 // 2FA画面を再表示して、処理終了 534 $this->show_two_factor_form( $post_data['login_token'] ); 920 $this->show_two_factor_form( 921 $login_token, 922 $_POST['auth_method'], 923 $_POST['has_recovery'], 924 $_POST['email_address'], 925 false 926 ); 535 927 } 536 928 … … 603 995 $log_data[] = array( 604 996 'name' => $data['user_login'], 605 'ip' => $this->get_client_ip( ''),997 'ip' => $this->get_client_ip(), 606 998 'status' => self::LOGIN_STATUS_FAILED, 607 'method' => 1,999 'method' => self::METHOD_PAGE, 608 1000 'login_at' => wp_date( 'Y-m-d H:i:s', $data['created'] ), // WPのタイムゾーンに変更して登録 609 1001 ); … … 812 1204 } 813 1205 } 1206 1207 /** 1208 * ユーザの2faログイン情報取得 1209 * 1210 * @param string $ip 1211 * 1212 * @return array $login_info 1213 */ 1214 private function get_2fa_login_info( string $ip ): array { 1215 global $wpdb; 1216 1217 $table_name = $wpdb->prefix . self::LOGIN_TABLE_NAME; 1218 $sql = "SELECT * FROM {$table_name} WHERE ip = %s"; 1219 1220 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1221 $row = $wpdb->get_row( $wpdb->prepare( $sql, $ip ), ARRAY_A ); 1222 1223 return $row ?? array(); 1224 } 1225 1226 /** 1227 * ユーザの2fa認証情報取得 1228 * 1229 * @param int $user_id 1230 * 1231 * @return array $auth_info 1232 */ 1233 public function get_2fa_auth_info( int $user_id ): array { 1234 global $wpdb; 1235 1236 $table_name = $wpdb->prefix . self::AUTH_TABLE_NAME; 1237 $sql = "SELECT * FROM {$table_name} WHERE user_id = %d"; 1238 1239 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1240 $row = $wpdb->get_row( $wpdb->prepare( $sql, $user_id ), ARRAY_A ); 1241 1242 if ( ! empty( $row ) && isset( $row['recovery'] ) ) { 1243 $row['recovery'] = maybe_unserialize( $row['recovery'] ); 1244 } 1245 1246 return $row ?? array(); 1247 } 1248 1249 /** 1250 * リカバリーコードの登録状況取得 1251 * 1252 * @param int $user_id 1253 * 1254 * @return bool true:登録済み、false:未登録 1255 */ 1256 public function has_recovery_codes( int $user_id ): bool { 1257 $auth_info = $this->get_2fa_auth_info( $user_id ); 1258 1259 if ( empty( $auth_info ) || is_null( $auth_info['recovery'] ) ) { 1260 return false; 1261 } 1262 1263 return true; 1264 } 1265 1266 /** 1267 * ユーザの2fa認証方法設定 1268 * 1269 * @param int $user_id 1270 * @param int $method 1271 * @param string $secret 1272 * 1273 * @return void 1274 */ 1275 public function setting_2fa_auth_info( int $user_id, int $method, string $secret ): void { 1276 // 認証情報取得 1277 $auth_info = $this->get_2fa_auth_info( $user_id ); 1278 1279 if ( ! empty( $auth_info ) ) { 1280 // 既に登録されている場合は更新 1281 $this->update_2fa_auth_method( $user_id, $method, $secret ); 1282 } else { 1283 // 登録されていない場合は新規登録 1284 $this->insert_2fa_auth_method( $user_id, $method, $secret ); 1285 } 1286 } 1287 1288 /** 1289 * ユーザの2fa認証方法更新 1290 * 1291 * @param int $user_id 1292 * @param int $method 1293 * @param string $secret 1294 * @return void 1295 */ 1296 private function update_2fa_auth_method( int $user_id, int $method, string $secret ): void { 1297 global $wpdb; 1298 1299 // 認証情報を更新 1300 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1301 $wpdb->update( 1302 $wpdb->prefix . 'cloudsecurewp_2fa_auth', 1303 array( 1304 'method' => $method, 1305 'secret' => $secret, 1306 ), 1307 array( 'user_id' => $user_id ) 1308 ); 1309 } 1310 1311 /** 1312 * ユーザの2fa認証方法登録 1313 * 1314 * @param int $user_id 1315 * @param int $method 1316 * @param string $secret 1317 * 1318 * @return void 1319 */ 1320 private function insert_2fa_auth_method( int $user_id, int $method, string $secret ): void { 1321 global $wpdb; 1322 1323 // 認証情報を新規登録 1324 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1325 $wpdb->insert( 1326 $wpdb->prefix . 'cloudsecurewp_2fa_auth', 1327 array( 1328 'user_id' => $user_id, 1329 'secret' => $secret, 1330 'recovery' => null, 1331 'method' => $method, 1332 ) 1333 ); 1334 } 1335 1336 /** 1337 * ログイン失敗回数インクリメント処理 1338 * 1339 * @return void 1340 * @throws Exception SQLエラー発生時. 1341 */ 1342 private function increment_fail_count(): void { 1343 global $wpdb; 1344 1345 // テーブル名取得 1346 $table_name = $wpdb->prefix . self::LOGIN_TABLE_NAME; 1347 1348 // 登録データ作成 1349 $ip = $this->get_client_ip(); 1350 $now_datetime = current_time( 'mysql' ); 1351 $data = array( 1352 'ip' => $ip, 1353 'status' => self::LOGIN_STATUS_FAILED, 1354 'failed_count' => 1, 1355 'login_at' => $now_datetime, 1356 ); 1357 1358 $row = $this->get_2fa_login_info( $ip ); 1359 1360 if ( empty( $row ) ) { 1361 // レコードが存在していない場合は新規登録 1362 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1363 $wpdb->insert( $table_name, $data ); 1364 } else { 1365 // レコードが存在している場合はカウントアップ 1366 $data['failed_count'] = (int) $row['failed_count'] + 1; 1367 1368 // 3回失敗ごとにステータスを無効化に更新 1369 if ( $data['failed_count'] % 3 === 0 ) { 1370 $data['status'] = self::LOGIN_STATUS_DISABLED; 1371 } 1372 1373 // 失敗回数とステータスを更新 1374 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1375 $wpdb->update( $table_name, $data, array( 'ip' => $ip ) ); 1376 } 1377 } 1378 1379 /** 1380 * ログインログ記録処理(失敗時) 1381 * 1382 * @param int $status ログインステータス 1383 * 1384 * @return void 1385 */ 1386 private function write_log( int $status ): void { 1387 global $wpdb; 1388 1389 // ログインログに記録 1390 $name = sanitize_text_field( $_POST['log'] ?? '' ); 1391 $ip = $this->get_client_ip(); 1392 $method = $this->login_log->is_xmlrpc() ? self::METHOD_XMLRPC : self::METHOD_PAGE; 1393 $this->login_log->write_log( $name, $ip, $status, $method ); 1394 } 1395 1396 /** 1397 * ログイン失敗回数リセット処理 1398 * 1399 * @return void 1400 */ 1401 private function reset_fail_count(): void { 1402 global $wpdb; 1403 1404 // テーブル名取得 1405 $table_name = $wpdb->prefix . self::LOGIN_TABLE_NAME; 1406 1407 // 登録データ作成 1408 $ip = $this->get_client_ip(); 1409 $now_datetime = current_time( 'mysql' ); 1410 $data = array( 1411 'ip' => $ip, 1412 'status' => self::LOGIN_STATUS_SUCCESS, 1413 'failed_count' => 0, 1414 'login_at' => $now_datetime, 1415 ); 1416 1417 $row = $this->get_2fa_login_info( $ip ); 1418 1419 if ( empty( $row ) ) { 1420 // レコードが存在していない場合は新規登録 1421 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1422 $wpdb->insert( $table_name, $data ); 1423 } else { 1424 if ( $row['status'] === self::LOGIN_STATUS_SUCCESS ) { 1425 // 既に成功状態の場合は何もしない 1426 return; 1427 } 1428 // 失敗回数とステータスを更新 1429 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1430 $wpdb->update( $table_name, $data, array( 'ip' => $ip ) ); 1431 1432 } 1433 } 1434 1435 /** 1436 * レートリミットの無効時間を取得 1437 * 無効ではない場合は0を返す 1438 * 1439 * @return int レートリミットの残り時間(分) 1440 */ 1441 private function two_factor_rate_limit(): int { 1442 // 現在のクライアントIPアドレスでレコードを取得 1443 $ip = $this->get_client_ip(); 1444 $row = $this->get_2fa_login_info( $ip ); 1445 1446 if ( ! empty( $row ) && (int) $row['status'] === self::LOGIN_STATUS_DISABLED ) { 1447 // 無効時間チェック 1448 $limit_time = self::LATE_LIMIT_TIME_LIST[ (int) $row['failed_count'] ] ?? self::LATE_LIMIT_TIME_MAX; 1449 $now_time = strtotime( current_time( 'mysql' ) ); 1450 $block_time = strtotime( $row['login_at'] ) + $limit_time; 1451 1452 if ( $now_time < $block_time ) { 1453 return (int) ceil( ( $block_time - $now_time ) / 60 ); 1454 } 1455 } 1456 1457 return 0; 1458 } 1459 1460 /** 1461 * メール最終送信時刻更新処理 1462 * 1463 * @param int $user_id 1464 * 1465 * @return void 1466 */ 1467 private function update_email_able_send_time( int $user_id ): void { 1468 // 送信可能時刻を更新 1469 $able_send_time = time() + self::EMAIL_SEND_LIMIT_TIME; 1470 update_user_meta( $user_id, self::TWO_FACTOR_EMAIL_SEND, $able_send_time ); 1471 } 1472 1473 /** 1474 * メール最終送信時刻リセット処理 1475 * 1476 * @param int $user_id 1477 * 1478 * @return void 1479 */ 1480 private function delete_email_able_send_time( int $user_id ): void { 1481 // 送信可能時刻をリセット 1482 update_user_meta( $user_id, self::TWO_FACTOR_EMAIL_SEND, 0 ); 1483 } 1484 1485 /** 1486 * メール最終送信時刻取得処理 1487 * 1488 * @param int $user_id 1489 * 1490 * @return int 最終送信時刻(unixタイムスタンプ) 1491 */ 1492 private function get_email_able_send_time( int $user_id ): int { 1493 // 最終送信時刻を取得 1494 $last_send = get_user_meta( $user_id, self::TWO_FACTOR_EMAIL_SEND, true ); 1495 1496 if ( ! $last_send ) { 1497 return 0; 1498 } 1499 1500 return (int) $last_send; 1501 } 1502 1503 /** 1504 * wp_usermetaから2fa関連データを取得 1505 * 1506 * @param int $last_umeta_id 1507 * @param int $batch_size 1508 * 1509 * @return array 1510 */ 1511 private function fetch_user_metas( int $last_umeta_id, int $batch_size ): array { 1512 global $wpdb; 1513 1514 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1515 $user_metas = $wpdb->get_results( 1516 $wpdb->prepare( 1517 "SELECT umeta_id, user_id, meta_value 1518 FROM {$wpdb->usermeta} 1519 WHERE meta_key = %s 1520 AND umeta_id > %d 1521 ORDER BY umeta_id ASC 1522 LIMIT %d", 1523 self::TWO_FACTOR_SECRET_KEY, 1524 $last_umeta_id, 1525 $batch_size 1526 ), 1527 ARRAY_A 1528 ); 1529 1530 return $user_metas ?? array(); 1531 } 1532 1533 /** 1534 * usermeta データをバルクインサート用データに変換 1535 * 1536 * @param array $user_metas 1537 * 1538 * @return array ['insert_data' => array, 'umeta_ids_map' => array] 1539 */ 1540 private function convert_user_metas_to_insert_data( array $user_metas ): array { 1541 $insert_data = array(); 1542 $umeta_ids_map = array(); 1543 1544 foreach ( $user_metas as $user_meta ) { 1545 $user_id = (int) $user_meta['user_id']; 1546 $umeta_id = (int) $user_meta['umeta_id']; 1547 $meta_value = $user_meta['meta_value']; 1548 1549 // Base32デコードしてバイナリに変換 1550 $binary_secret = CloudSecureWP_Time_Based_One_Time_Password::base32_decode( $meta_value ); 1551 1552 // バイナリデータを16進数に変換 1553 $hex_secret = bin2hex( $binary_secret ); 1554 1555 // データを蓄積 1556 $insert_data[] = array( 1557 'user_id' => $user_id, 1558 'secret' => $hex_secret, 1559 'recovery' => null, 1560 'method' => self::USER_AUTH_METHOD_APP, 1561 ); 1562 1563 // umeta_idとuser_idのマッピングを保存 1564 $umeta_ids_map[ $user_id ] = $umeta_id; 1565 } 1566 1567 return array( 1568 'insert_data' => $insert_data, 1569 'umeta_ids_map' => $umeta_ids_map, 1570 ); 1571 } 1572 1573 /** 1574 * 2fa認証データのバルクインサート 1575 * (呼び出し元でトランザクションを管理すること) 1576 * 1577 * @param array $data_list 1578 * 1579 * @return array 失敗したuser_idのリスト 1580 * @throws Exception SQLエラー発生時. 1581 */ 1582 private function bulk_insert_2fa_auth( array $data_list ): array { 1583 if ( empty( $data_list ) ) { 1584 return array(); 1585 } 1586 1587 global $wpdb; 1588 1589 $table_name = $wpdb->prefix . self::AUTH_TABLE_NAME; 1590 1591 // 挿入を試みるuser_idのリスト 1592 $target_user_ids = array_column( $data_list, 'user_id' ); 1593 1594 // プレースホルダーと値の準備 1595 $values = array(); 1596 $placeholders = array(); 1597 1598 foreach ( $data_list as $data ) { 1599 $values[] = $data['user_id']; 1600 $values[] = $data['secret']; 1601 $values[] = $data['method']; 1602 $placeholders[] = '(%d, %s, null, %d)'; 1603 } 1604 1605 // SQL実行(INSERT IGNOREで重複などのエラーを無視) 1606 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1607 $result = $wpdb->query( 1608 $wpdb->prepare( 1609 "INSERT IGNORE INTO {$table_name} 1610 (`user_id`, `secret`, `recovery`, `method`) 1611 VALUES " . implode( ', ', $placeholders ), 1612 $values 1613 ) 1614 ); 1615 1616 // SQLエラーチェック(INSERT IGNOREは重複エラーを返さないが、その他のエラーはチェック) 1617 if ( $result === false || ! empty( $wpdb->last_error ) ) { 1618 throw new Exception( 'Failed to bulk insert 2fa auth data: ' . $wpdb->last_error ); 1619 } 1620 1621 // 失敗したuser_id(挿入されなかったID)を特定 1622 // INSERT IGNOREは重複時に0行を挿入するため、 1623 // 挿入されなかった数 = 試行数 - 実際に挿入された行数 1624 $failed_count = count( $target_user_ids ) - $result; 1625 1626 // 失敗したIDを特定したい場合は、挿入後に存在確認が必要 1627 if ( $failed_count > 0 ) { 1628 // 挿入後に実際に存在するuser_idを確認 1629 $placeholders_check = implode( ', ', array_fill( 0, count( $target_user_ids ), '%d' ) ); 1630 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1631 $inserted_user_ids = $wpdb->get_col( 1632 $wpdb->prepare( 1633 "SELECT user_id FROM {$table_name} WHERE user_id IN ($placeholders_check)", 1634 $target_user_ids 1635 ) 1636 ); 1637 1638 // 失敗したuser_idを特定 1639 $failed_user_ids = array_diff( $target_user_ids, $inserted_user_ids ); 1640 return array_values( $failed_user_ids ); 1641 } 1642 1643 return array(); 1644 } 1645 1646 /** 1647 * wp_usermetaから指定されたレコードを削除 1648 * (呼び出し元でトランザクションを管理すること) 1649 * 1650 * @param array $umeta_ids 1651 * 1652 * @return void 1653 * @throws Exception SQLエラー発生時. 1654 */ 1655 private function delete_user_metas( array $umeta_ids ): void { 1656 if ( empty( $umeta_ids ) ) { 1657 return; 1658 } 1659 1660 global $wpdb; 1661 1662 $delete_placeholders = implode( ', ', array_fill( 0, count( $umeta_ids ), '%d' ) ); 1663 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching 1664 $result = $wpdb->query( 1665 $wpdb->prepare( 1666 "DELETE FROM {$wpdb->usermeta} WHERE umeta_id IN ($delete_placeholders)", 1667 $umeta_ids 1668 ) 1669 ); 1670 1671 // 削除エラーチェック 1672 if ( $result === false || ! empty( $wpdb->last_error ) ) { 1673 throw new Exception( 'Failed to delete usermeta: ' . $wpdb->last_error ); 1674 } 1675 } 1676 1677 /** 1678 * 2fa既存ユーザーデータの移行処理 1679 * 1680 * @return void 1681 */ 1682 public function migrate_2fa_user_data(): void { 1683 global $wpdb; 1684 1685 $batch_size = self::CLEANUP_BATCH_SIZE; 1686 $last_umeta_id = 0; 1687 1688 while ( true ) { 1689 try { 1690 // wp_usermetaからシークレットキーをバッチサイズごとに取得 1691 $user_metas = $this->fetch_user_metas( $last_umeta_id, $batch_size ); 1692 1693 // 取得するレコードがなくなったら終了 1694 if ( empty( $user_metas ) ) { 1695 break; 1696 } 1697 1698 // 最後に取得したumeta_idを更新 1699 $last_umeta_id = end( $user_metas )['umeta_id']; 1700 1701 // バルクインサート用のデータを準備 1702 $conversion_result = $this->convert_user_metas_to_insert_data( $user_metas ); 1703 $insert_data = $conversion_result['insert_data']; 1704 $umeta_ids_map = $conversion_result['umeta_ids_map']; 1705 1706 // 登録するデータがない場合は次のバッチへ 1707 if ( empty( $insert_data ) ) { 1708 continue; 1709 } 1710 1711 // トランザクション開始 1712 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1713 $wpdb->query( 'START TRANSACTION' ); 1714 1715 // バルクインサート実行(失敗したuser_idのリストを取得) 1716 $failed_user_ids = $this->bulk_insert_2fa_auth( $insert_data ); 1717 1718 // 成功したuser_idを特定 1719 $target_user_ids = array_column( $insert_data, 'user_id' ); 1720 $successful_user_ids = array_diff( $target_user_ids, $failed_user_ids ); 1721 1722 // 成功したレコードのumeta_idを取得 1723 $successful_umeta_ids = array(); 1724 foreach ( $successful_user_ids as $user_id ) { 1725 if ( isset( $umeta_ids_map[ $user_id ] ) ) { 1726 $successful_umeta_ids[] = $umeta_ids_map[ $user_id ]; 1727 } 1728 } 1729 1730 // 成功したレコードのみwp_usermetaから削除 1731 $this->delete_user_metas( $successful_umeta_ids ); 1732 1733 // トランザクションコミット 1734 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1735 $wpdb->query( 'COMMIT' ); 1736 1737 } catch ( Exception $e ) { 1738 // エラー発生時はロールバック 1739 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1740 $wpdb->query( 'ROLLBACK' ); 1741 break; 1742 } 1743 } 1744 } 1745 1746 /** 1747 * 2faログインテーブル作成 1748 * 1749 * @return void 1750 */ 1751 private function create_login_table(): void { 1752 global $wpdb; 1753 $table_name = $wpdb->prefix . self::LOGIN_TABLE_NAME; 1754 $table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); 1755 1756 if ( is_null( $table ) ) { 1757 $charset_collate = $wpdb->get_charset_collate(); 1758 1759 $sql = "CREATE TABLE {$table_name} ( 1760 ip varchar( 39 ) NOT NULL DEFAULT '', 1761 status int NOT NULL DEFAULT 0, 1762 failed_count int NOT NULL DEFAULT 0, 1763 login_at datetime NOT NULL, 1764 PRIMARY KEY (ip) 1765 ) {$charset_collate}"; 1766 1767 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 1768 dbDelta( $sql ); 1769 } 1770 } 1771 1772 /** 1773 * 2fa認証テーブル作成 1774 * 1775 * @return void 1776 */ 1777 private function create_auth_table(): void { 1778 global $wpdb; 1779 $table_name = $wpdb->prefix . self::AUTH_TABLE_NAME; 1780 $table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); 1781 1782 if ( is_null( $table ) ) { 1783 $charset_collate = $wpdb->get_charset_collate(); 1784 1785 $sql = "CREATE TABLE {$table_name} ( 1786 user_id bigint(20) UNSIGNED NOT NULL, 1787 secret varchar(255) NOT NULL, 1788 recovery longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, 1789 method int(11) UNSIGNED NOT NULL DEFAULT 1, 1790 PRIMARY KEY (user_id) 1791 ) {$charset_collate}"; 1792 1793 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 1794 dbDelta( $sql ); 1795 } 1796 } 1797 1798 /** 1799 * 2段階認証のテーブル作成とデータ移行を一括実行 1800 * 1801 * @return void 1802 */ 1803 public function setup_2fa_tables(): void { 1804 $this->create_login_table(); 1805 $this->create_auth_table(); 1806 $this->migrate_2fa_user_data(); 1807 } 1808 1809 /** 1810 * データ移行漏れ修復処理 1811 * 1812 * @param int $user_id 1813 * 1814 * @return array 1815 */ 1816 public function repair_migration_gaps( int $user_id ): array { 1817 global $wpdb; 1818 1819 // 2fa認証テーブルが存在しない場合は作成 1820 $this->create_auth_table(); 1821 // 2faログインテーブルが存在しない場合は作成 1822 $this->create_login_table(); 1823 1824 // 2fa認証情報を取得 1825 $auth_info = $this->get_2fa_auth_info( $user_id ); 1826 if ( count( $auth_info ) !== 0 ) { 1827 // 既に認証情報が存在する場合は何もしない 1828 return $auth_info; 1829 } 1830 1831 // wp_usermetaからシークレットキーを取得 1832 $secret = get_user_meta( $user_id, self::TWO_FACTOR_SECRET_KEY, true ); 1833 if ( ! $secret ) { 1834 // シークレットキーが存在しない場合は何もしない 1835 return []; 1836 } 1837 1838 try { 1839 // トランザクション開始 1840 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1841 $wpdb->query( 'START TRANSACTION' ); 1842 1843 // 登録データ作成 1844 // Base32デコードしてバイナリに変換 1845 $binary_secret = CloudSecureWP_Time_Based_One_Time_Password::base32_decode( $secret ); 1846 // バイナリデータを16進数に変換 1847 $hex_secret = bin2hex( $binary_secret ); 1848 1849 // 2fa認証情報を登録 1850 $this->insert_2fa_auth_method( $user_id, self::USER_AUTH_METHOD_APP, $hex_secret ); 1851 1852 // シークレットキーをwp_usermetaから削除 1853 delete_user_meta( $user_id, self::TWO_FACTOR_SECRET_KEY ); 1854 1855 // トランザクションコミット 1856 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1857 $wpdb->query( 'COMMIT' ); 1858 $auth_info = $this->get_2fa_auth_info( $user_id ); 1859 return $auth_info; 1860 } catch ( Exception $e ) { 1861 // エラー発生時はロールバック 1862 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1863 $wpdb->query( 'ROLLBACK' ); 1864 return []; 1865 } 1866 } 1867 1868 /** 1869 * AJAX: 秘密鍵を生成(アプリ認証時に使用) 1870 * 1871 * @return void 1872 */ 1873 public function ajax_generate_key(): void { 1874 // nonceチェック 1875 check_ajax_referer( $this->get_feature_key() . '_csrf', 'nonce', true ); 1876 1877 // 秘密鍵を生成 1878 $secret_key_data = CloudSecureWP_Time_Based_One_Time_Password::generate_secret_key(); 1879 1880 wp_send_json_success( $secret_key_data ); 1881 } 1882 1883 /** 1884 * AJAX: 秘密鍵を生成してメール送信(メール認証時に使用) 1885 * 1886 * @return void 1887 */ 1888 public function ajax_generate_key_and_send_email(): void { 1889 // nonceチェック 1890 check_ajax_referer( $this->get_feature_key() . '_csrf', 'nonce', true ); 1891 1892 // 返却JSONレスポンス初期化 1893 $json_response = [ 1894 'is_send_email' => false, 1895 'remaining_seconds' => 0, 1896 ]; 1897 1898 $user_id = get_current_user_id(); 1899 1900 // 最終送信時刻から30秒以内ならそのまま返す 1901 $able_send_time = $this->get_email_able_send_time( $user_id ); 1902 $now_time = time(); 1903 $remaining_seconds = $able_send_time - $now_time; 1904 if ( $remaining_seconds > 0 ) { 1905 $json_response['remaining_seconds'] = $remaining_seconds; 1906 wp_send_json_success( $json_response ); 1907 } 1908 1909 try { 1910 // 秘密鍵を生成 1911 $secret_key_data = CloudSecureWP_Time_Based_One_Time_Password::generate_secret_key(); 1912 // 認証コードを生成 1913 $code = CloudSecureWP_Time_Based_One_Time_Password::create_code_for_email( $secret_key_data['hex'], self::AUTH_EMAIL_INTERVAL ); 1914 // メール送信 1915 $this->send_code( $user_id, $code, self::AUTH_EMAIL_INTERVAL, 'setting' ); 1916 // 最終送信時刻を更新 1917 $this->update_email_able_send_time( $user_id ); 1918 1919 // 秘密鍵を保存 1920 $transient_key = '2fa_setup_secret_' . $user_id; 1921 set_transient( $transient_key, $secret_key_data['hex'], 180 ); 1922 1923 $json_response['is_send_email'] = true; 1924 $json_response['remaining_seconds'] = self::EMAIL_SEND_LIMIT_TIME; 1925 wp_send_json_success( $json_response ); 1926 1927 } catch ( Exception $e ) { 1928 wp_send_json_error( $json_response ); 1929 } 1930 } 1931 1932 /** 1933 * AJAX: 認証コードの検証と保存 1934 * 1935 * @return void 1936 */ 1937 public function ajax_verify_auth_code(): void { 1938 // nonceチェック 1939 check_ajax_referer( $this->get_feature_key() . '_csrf', 'nonce', true ); 1940 1941 // 返却JSONレスポンス初期化 1942 $json_response = [ 1943 'has_recovery' => false, 1944 ]; 1945 1946 $user_id = get_current_user_id(); 1947 1948 $method = sanitize_text_field( $_POST['method'] ); 1949 $interval = ( $method === 'app' ) ? self::AUTH_APP_INTERVAL : self::AUTH_EMAIL_INTERVAL; 1950 $auth_method = ( $method === 'app' ) ? self::USER_AUTH_METHOD_APP : self::USER_AUTH_METHOD_EMAIL; 1951 $code = isset( $_POST['code'] ) ? sanitize_text_field( $_POST['code'] ) : ''; 1952 1953 if ( $method === 'app' ) { 1954 $secret_key = isset( $_POST['secret_key'] ) ? sanitize_text_field( $_POST['secret_key'] ) : ''; 1955 } else { 1956 $transient_key = '2fa_setup_secret_' . $user_id; 1957 $secret_key = get_transient( $transient_key ); 1958 if ( ! $secret_key ) { 1959 wp_send_json_error( $json_response ); 1960 } 1961 } 1962 1963 if ( ! CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret_key, $code, $interval ) ) { 1964 wp_send_json_error( $json_response ); 1965 } 1966 1967 // 認証成功 - データベースに保存 1968 try { 1969 global $wpdb; 1970 1971 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1972 $wpdb->query( 'START TRANSACTION' ); 1973 1974 // 2fa認証情報を設定 1975 $this->setting_2fa_auth_info( $user_id, $auth_method, $secret_key ); 1976 1977 // メール認証の場合 1978 if ( $method === 'email' ) { 1979 // 一時保存していた秘密鍵を削除 1980 delete_transient( $transient_key ); 1981 // メール認証の送信可能時間をリセット 1982 $this->delete_email_able_send_time( $user_id ); 1983 } 1984 1985 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1986 $wpdb->query( 'COMMIT' ); 1987 1988 // リカバリーコードの設定状況を取得 1989 $has_recovery = $this->has_recovery_codes( $user_id ); 1990 $json_response['has_recovery'] = $has_recovery; 1991 wp_send_json_success( $json_response ); 1992 } catch ( Exception $e ) { 1993 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery 1994 $wpdb->query( 'ROLLBACK' ); 1995 1996 wp_send_json_error( $json_response ); 1997 } 1998 } 1999 2000 /** 2001 * AJAX: リカバリーコード生成 2002 * 2003 * @return void 2004 */ 2005 public function ajax_generate_recovery_codes(): void { 2006 // nonceチェック 2007 check_ajax_referer( $this->get_feature_key() . '_csrf', 'nonce', true ); 2008 2009 // 返却JSONレスポンス初期化 2010 $json_response = [ 2011 'codes' => [], 2012 ]; 2013 2014 // 2段階認証が設定されているかチェック 2015 try { 2016 $user_id = get_current_user_id(); 2017 $auth_info = $this->get_2fa_auth_info( $user_id ); 2018 if ( count( $auth_info ) === 0 ) { 2019 wp_send_json_error( $json_response ); 2020 } 2021 2022 // リカバリーコードを生成 2023 $codes = CloudSecureWP_Recovery_Codes::initialize_codes( $user_id ); 2024 if ( count( $codes ) === 0 ) { 2025 wp_send_json_error( $json_response ); 2026 } 2027 2028 // 平文コードを返却(これは1度だけ表示される) 2029 $json_response['codes'] = $codes; 2030 wp_send_json_success( $json_response ); 2031 2032 } catch ( Exception $e ) { 2033 wp_send_json_error( $json_response ); 2034 } 2035 } 2036 2037 /** 2038 * ユーザーに認証コードをメール送信 2039 * 2040 * @param int $user_id 2041 * @param string $code 2042 * @param int $time_step 時間間隔(秒) 2043 * @param string $status 'login' または 'setting' 2044 * 2045 * @return void 2046 */ 2047 private function send_code( int $user_id, string $code, int $time_step, string $status ): void { 2048 $user = get_userdata( $user_id ); 2049 if ( ! $user ) { 2050 return; 2051 } 2052 $expire = $time_step / 60; 2053 $to = $user->user_email; 2054 $subject = '2段階認証コード'; 2055 2056 if ( $status === 'login' ) { 2057 $body = "ユーザー {$user->user_login} が" . get_bloginfo( 'name' ) . " にログインしようとしています。\n"; 2058 $body .= "ログインを完了するには、以下の2段階認証コードを入力してください。\n\n"; 2059 } else { 2060 $body = "ユーザー {$user->user_login} が" . get_bloginfo( 'name' ) . "で2段階認証を設定しています。\n"; 2061 $body .= "セットアップを完了するには、以下の2段階認証コードを入力してください。\n\n"; 2062 } 2063 2064 $body .= "2段階認証コード: {$code}\n\n"; 2065 $body .= "このコードは{$expire}分間有効です。\n"; 2066 $body .= "もしこのメールに心当たりがない場合は、第三者があなたのパスワードを使用してログインを試みた可能性があります。\n"; 2067 $body .= "速やかにパスワードを変更することをお勧めします。\n\n"; 2068 $body .= "--\n"; 2069 $body .= "CloudSecure WP Security\n"; 2070 2071 $this->wp_send_mail( $to, esc_html( $subject ), esc_html( $body ) ); 2072 } 814 2073 } -
cloudsecure-wp-security/trunk/modules/waf-engine.php
r3400140 r3462167 508 508 $results = array( 509 509 'is_matched' => false, 510 'match_string' => '' 510 'match_string' => '', 511 511 ); 512 $matches = array();512 $matches = array(); 513 513 514 514 // リクエスト情報配列のkeyとvalueの変換 … … 541 541 $results = array( 542 542 'is_matched' => false, 543 'match_string' => '' 543 'match_string' => '', 544 544 ); 545 545 $matches = array(); … … 631 631 * @param array $request_items 632 632 * @param array $remove_rules 633 * @return bool 634 */ 635 public function is_remove_rule( $rule_id, $request_items, $remove_rules, $acf_post_types ): bool { 636 $is_rule_removed = false; 633 * @return array 634 */ 635 public function is_remove_rule( $rule_id, $request_items, $remove_rules, $acf_post_types ): array { 636 $is_rule_removed = false; 637 $modify_remove_variables = array(); 637 638 638 639 if ( isset( $remove_rules['woocommerce'] ) ) { … … 657 658 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) { 658 659 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) { 660 $action = $request_items['args']['action'] ?? ''; 661 if ( $action === 'update-widget' ) { 662 // actionがupdate-widgetの場合はルール全体を除外(cocoon) 663 $is_rule_removed = true; 664 } else { 665 // それ以外の場合はcustomized, customize_changeset_dataキーのみ除外 666 $modify_remove_variables['args'] = array( 'customized', 'customize_changeset_data' ); 667 } 668 } 669 } 670 // ウィジェット保存時(cocoonテーマ)の除外 671 } elseif ( preg_match( '/wp-admin\/widgets\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) { 672 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) { 673 if ( ( $request_items['args']['action'] ?? '' ) === 'save-widget' ) { 674 $is_rule_removed = true; 675 } 676 } 677 // メニュー操作時(cocoonテーマ)の除外 678 } elseif ( preg_match( '/wp-admin\/nav-menus\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) { 679 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) { 680 $action = $request_items['args']['action'] ?? ''; 681 if ( $action === 'update' || $action === 'edit' ) { 659 682 $is_rule_removed = true; 660 683 } … … 664 687 665 688 if ( isset( $remove_rules['rest_api'] ) ) { 666 // 投稿・編集 の操作は特定のルールを除外する(rest_api)667 if ( preg_match( '/templates|blocks|template-parts|navigation| global-styles|pages|posts|batch/', $request_items['request_filename'] ) === 1 ) {689 // 投稿・編集(templates, blocks, template-parts, navigation, pages, posts)の場合はcontentキーを除外 690 if ( preg_match( '/templates|blocks|template-parts|navigation|pages|posts/', $request_items['request_filename'] ) === 1 ) { 668 691 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 669 692 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 670 $is_rule_removed = true; 693 $modify_remove_variables['args'] = array( 'content' ); 694 } 695 } 696 697 // global-styles の場合はstylesキーを除外 698 } elseif ( preg_match( '/global-styles/', $request_items['request_filename'] ) === 1 ) { 699 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 700 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 701 $modify_remove_variables['args'] = array( '/^styles/' ); 702 } 703 } 704 705 // batch の場合はrequestsキーを除外 706 } elseif ( preg_match( '/batch/', $request_items['request_filename'] ) === 1 ) { 707 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 708 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 709 $modify_remove_variables['args'] = array( '/^requests/' ); 671 710 } 672 711 } … … 675 714 } elseif ( preg_match( '/wp-admin\/post\.php/', $request_items['request_filename'] ) === 1 ) { 676 715 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 677 if ( ( $request_items['args']['action'] ?? '' ) === 'editpost' ) { 678 $is_rule_removed = true; 679 } 680 } 681 682 // nishikiルール除外 716 $action = $request_items['args']['action'] ?? ''; 717 if ( $action === 'editpost' ) { 718 $is_rule_removed = true; 719 } elseif ( $action === 'post-quickdraft-save' ) { 720 $modify_remove_variables['args'] = array( 'content' ); 721 } 722 } 723 724 // カスタマイズ機能からの投稿作成時の除外 725 } elseif ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) { 726 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 727 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) { 728 $action = $request_items['args']['action'] ?? ''; 729 if ( $action === 'customize-nav-menus-insert-auto-draft' ) { 730 $modify_remove_variables['args'] = array( '/^params/' ); 731 } 732 } 733 } 734 735 // nishiki の場合はcontentキーを除外 683 736 } elseif ( preg_match( '/wp\/v2\/nishiki_pro_(patterns|content)/', $request_items['request_filename'] ) === 1 ) { 684 737 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 685 738 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 686 $ is_rule_removed = true;687 } 688 } 689 690 // xerite ルール除外739 $modify_remove_variables['args'] = array( 'content' ); 740 } 741 } 742 743 // xerite の場合はcontentキーを除外 691 744 } elseif ( preg_match( '/wp\/v2\/xw_block_patterns/', $request_items['request_filename'] ) === 1 ) { 692 745 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 693 746 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 694 $ is_rule_removed = true;695 } 696 } 697 698 // Lightning ルール除外747 $modify_remove_variables['args'] = array( 'content' ); 748 } 749 } 750 751 // Lightning の場合はcontentキーを除外 699 752 } elseif ( preg_match( '/wp\/v2\/(cta|vk-block-patterns)/', $request_items['request_filename'] ) === 1 ) { 700 753 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 701 754 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 702 $ is_rule_removed = true;703 } 704 } 705 706 // SWELL ルール除外755 $modify_remove_variables['args'] = array( 'content' ); 756 } 757 } 758 759 // SWELL の場合はcontentキーを除外 707 760 } elseif ( preg_match( '/wp\/v2\/(lp|blog_parts)/', $request_items['request_filename'] ) === 1 ) { 708 761 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 709 762 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 710 $is_rule_removed = true; 711 } 712 } 713 714 // Advanced Custom Fields除外 715 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする 716 } elseif ( preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $request_items['request_filename'] ) === 1 ) { 763 $modify_remove_variables['args'] = array( 'content' ); 764 } 765 } 766 767 // Snow Monkey の場合はcontentキーを除外 768 } elseif ( preg_match( '/wp\/v2\/snow-monkey-search/', $request_items['request_filename'] ) === 1 ) { 717 769 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 718 770 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 719 $is_rule_removed = true; 720 } 721 } 722 } 723 } 724 725 if ( isset( $remove_rules['coccon'] ) ) { 726 // cocconルール除外 727 if ( preg_match( '/wp-admin\/admin\.php\?page\=theme-(settings|func-text|ranking|affiliate-tag)/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) { 728 if ( in_array( $rule_id, $remove_rules['coccon'], true ) ) { 771 $modify_remove_variables['args'] = array( 'content' ); 772 } 773 } 774 775 // Advanced Custom Fields の場合はcontentキーを除外 776 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする 777 } elseif ( ! empty( $acf_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $request_items['request_filename'] ) === 1 ) { 778 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) { 779 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) { 780 $modify_remove_variables['args'] = array( 'content' ); 781 } 782 } 783 } 784 } 785 786 // cocoonテーマでの除外処理(theme-func-text, theme-settings, theme-ranking, theme-affiliate-tag) 787 if ( isset( $remove_rules['cocoon'] ) ) { 788 $referer = $_SERVER['HTTP_REFERER'] ?? ''; 789 if ( preg_match( '/wp-admin\/admin\.php\?page\=theme-(func-text|settings|ranking|affiliate-tag)/', $referer ) === 1 ) { 790 if ( in_array( $rule_id, $remove_rules['cocoon'], true ) ) { 729 791 $action = $request_items['args']['action'] ?? ''; 730 792 if ( $action === 'new' || $action === 'edit' ) { … … 751 813 752 814 if ( isset( $remove_rules['vkexunit'] ) ) { 753 // vkExUnitルール除外 (メイン設定)815 // vkExUnitルール除外(メイン設定) 754 816 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_main_setting/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) { 755 817 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) { … … 760 822 } 761 823 762 // vkExUnitルール除外 (cssカスタマイズ)824 // vkExUnitルール除外(cssカスタマイズ) 763 825 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_css_customize/', $_SERVER['REQUEST_URI'] ?? '' ) === 1 ) { 764 826 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) { … … 821 883 } 822 884 } 823 return $is_rule_removed; 885 return array( 886 'is_removed' => $is_rule_removed, 887 'modify_remove_variables' => $modify_remove_variables, 888 ); 824 889 } 825 890 … … 916 981 917 982 // 特定の操作の場合、特定のルールを除外する 918 $ is_rule_removed= $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types );919 920 if ( $ is_rule_removed) {983 $remove_rule_result = $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types ); 984 985 if ( $remove_rule_result['is_removed'] ) { 921 986 continue; 987 } 988 989 // ルールのremove_variablesを動的に変更 990 if ( ! empty( $remove_rule_result['modify_remove_variables'] ) ) { 991 $waf_rule['remove_variables'] = array_merge_recursive( 992 $waf_rule['remove_variables'], 993 $remove_rule_result['modify_remove_variables'] 994 ); 922 995 } 923 996 -
cloudsecure-wp-security/trunk/modules/waf.php
r3230168 r3462167 12 12 private const KEY_AVAILABLE_RULES = self::KEY_FEATURE . '_available_rules'; 13 13 private const RULES_CATEGORY = self::KEY_FEATURE . '_rules_category'; 14 private const RULES_CATEGORY_VA RUES = array( 1, 2, 4, 8, 16 );14 private const RULES_CATEGORY_VALUES = array( 1, 2, 4, 8, 16 ); 15 15 private const RULES_CATEGORY_NAMES = array( 16 16 'SQLインジェクション', … … 78 78 self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES[1], 79 79 self::KEY_SEND_AT => array(), 80 self::KEY_AVAILABLE_RULES => 63,80 self::KEY_AVAILABLE_RULES => 31, 81 81 ); 82 82 … … 93 93 $ret = array( 94 94 self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES, 95 self::KEY_AVAILABLE_RULES => self::RULES_CATEGORY_VA RUES,95 self::KEY_AVAILABLE_RULES => self::RULES_CATEGORY_VALUES, 96 96 self::RULES_CATEGORY => array( 97 self::RULES_CATEGORY_VA RUES[0] => self::RULES_CATEGORY_NAMES[0],98 self::RULES_CATEGORY_VA RUES[1] => self::RULES_CATEGORY_NAMES[1],99 self::RULES_CATEGORY_VA RUES[2] => self::RULES_CATEGORY_NAMES[2],100 self::RULES_CATEGORY_VA RUES[3] => self::RULES_CATEGORY_NAMES[3],101 self::RULES_CATEGORY_VA RUES[4] => self::RULES_CATEGORY_NAMES[4],97 self::RULES_CATEGORY_VALUES[0] => self::RULES_CATEGORY_NAMES[0], 98 self::RULES_CATEGORY_VALUES[1] => self::RULES_CATEGORY_NAMES[1], 99 self::RULES_CATEGORY_VALUES[2] => self::RULES_CATEGORY_NAMES[2], 100 self::RULES_CATEGORY_VALUES[3] => self::RULES_CATEGORY_NAMES[3], 101 self::RULES_CATEGORY_VALUES[4] => self::RULES_CATEGORY_NAMES[4], 102 102 ), 103 103 ); … … 277 277 $settings = $this->get_settings(); 278 278 $send_at = $settings[ self::KEY_SEND_AT ] ?? array(); 279 $tmp_send_at = 0; 279 280 280 281 if ( ! empty( $send_at ) && is_array( $send_at ) ) { … … 285 286 } 286 287 } 287 } else {288 $tmp_send_at = 0;289 288 } 290 289 … … 320 319 'rest_api' => array( '950004', '950001', '950007', '950006' ), 321 320 'comment' => array( '950004' ), 322 'coc con' => array( '950004' ),321 'cocoon' => array( '950004' ), 323 322 'emanon' => array( '950004', '950001', '950007' ), 324 323 'vkexunit' => array( '950004', '950001', '950007' ), -
cloudsecure-wp-security/trunk/readme.txt
r3444508 r3462167 4 4 Requires at least: 5.3.15 5 5 Tested up to: 6.9 6 Stable tag: 1. 3.246 Stable tag: 1.4.0 7 7 License: GPLv2 or later 8 8 License URI: http://www.gnu.org/licenses/gpl-2.0.html … … 107 107 == Changelog == 108 108 109 = 1.4.0 = 110 * 2段階認証機能にメール認証およびリカバリーコードの追加 111 * 2段階認証によるログインのセキュリティ強化 112 * 軽微な修正 113 109 114 = 1.3.24 = 110 115 * 軽微な修正 -
cloudsecure-wp-security/trunk/uninstall.php
r3124889 r3462167 27 27 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}cloudsecurewp_server_error" ); 28 28 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}cloudsecurewp_waf_log" ); 29 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}cloudsecurewp_2fa_auth" ); 30 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}cloudsecurewp_2fa_login" ); 29 31 } 30 32
Note: See TracChangeset
for help on using the changeset viewer.