admin
2 weeks ago
cli
2 weeks ago
lib
1 month ago
captcha.php
2 weeks ago
cloudsecure-wp.php
6 days ago
common.php
4 months ago
config.php
2 years ago
disable-access-system-file.php
1 month ago
disable-author-query.php
2 weeks ago
disable-login.php
1 month ago
disable-restapi.php
2 weeks ago
disable-xmlrpc.php
1 year ago
htaccess.php
4 months ago
login-log.php
1 month ago
login-notification.php
3 months ago
protect-rest-batch.php
1 month ago
rename-login-page.php
4 months ago
restrict-admin-page.php
3 months ago
server-error-notification.php
3 months ago
two-factor-authentication.php
6 days ago
unify-messages.php
2 years ago
update-notice.php
9 months ago
waf-engine.php
6 days ago
waf.php
1 month ago
waf.php
477 lines
| 1 | <?php |
| 2 | |
| 3 | if ( ! defined( 'ABSPATH' ) ) { |
| 4 | exit; |
| 5 | } |
| 6 | |
| 7 | class CloudSecureWP_Waf extends CloudSecureWP_Waf_Engine { |
| 8 | private const KEY_FEATURE = 'waf'; |
| 9 | private const KEY_SEND_ADMIN_MAIL = self::KEY_FEATURE . '_send_admin_mail'; |
| 10 | private const SEND_ADMIN_MAIL_VALUES = array( 1, 2 ); // 無効、有効 . |
| 11 | private const KEY_SEND_AT = self::KEY_FEATURE . '_send_at'; |
| 12 | private const KEY_AVAILABLE_RULES = self::KEY_FEATURE . '_available_rules'; |
| 13 | private const KEY_DENY_BACKTRACK_ERROR = self::KEY_FEATURE . '_backtrack_error'; |
| 14 | private const RULES_CATEGORY = self::KEY_FEATURE . '_rules_category'; |
| 15 | private const RULES_CATEGORY_VALUES = array( 1, 2, 4, 8, 16 ); |
| 16 | private const RULES_CATEGORY_NAMES = array( |
| 17 | 'SQLインジェクション', |
| 18 | 'クロスサイトスクリプティング', |
| 19 | 'OSコマンドインジェクション', |
| 20 | 'コードインジェクション', |
| 21 | 'メールヘッダインジェクション', |
| 22 | ); |
| 23 | private const BACKTRACK_ATTACK_NAME = '検査上限エラー'; |
| 24 | private const TABLE_NAME = 'cloudsecurewp_waf_log'; |
| 25 | private const COLUMN_ID = 'id'; |
| 26 | private const COLUMN_ACCESS_AT = 'access_at'; |
| 27 | private const COLUMN_ATTACK = 'attack'; |
| 28 | private const COLUMN_URL = 'url'; |
| 29 | private const COLUMN_MATCHED = 'matched'; |
| 30 | private const COLUMN_IP = 'ip'; |
| 31 | private const COLUMNS = array( |
| 32 | self::COLUMN_ID => 'ID', |
| 33 | self::COLUMN_ACCESS_AT => '日時', |
| 34 | self::COLUMN_ATTACK => '検知� |
| 35 | 容', |
| 36 | self::COLUMN_URL => '検知されたページのURL', |
| 37 | self::COLUMN_MATCHED => '検知されたデータ', |
| 38 | self::COLUMN_IP => 'アクセス� |
| 39 | �IPアドレス', |
| 40 | ); |
| 41 | private const MAX_LOG = 10000; |
| 42 | private $config; |
| 43 | private $waf_rules; |
| 44 | |
| 45 | |
| 46 | |
| 47 | function __construct( array $info, CloudSecureWP_Config $config ) { |
| 48 | parent::__construct( $info ); |
| 49 | $this->config = $config; |
| 50 | $this->waf_rules = new CloudSecureWP_Waf_Rules(); |
| 51 | } |
| 52 | |
| 53 | |
| 54 | /** |
| 55 | * 機能毎のKEY取得 |
| 56 | * |
| 57 | * @return string |
| 58 | */ |
| 59 | public function get_feature_key(): string { |
| 60 | return self::KEY_FEATURE; |
| 61 | } |
| 62 | |
| 63 | |
| 64 | /** |
| 65 | * 有効無効判定 |
| 66 | * |
| 67 | * @return bool |
| 68 | */ |
| 69 | public function is_enabled(): bool { |
| 70 | return $this->config->get( $this->get_feature_key() ) === 't' ? true : false; |
| 71 | } |
| 72 | |
| 73 | |
| 74 | /** |
| 75 | * 初期設定値取得 |
| 76 | * |
| 77 | * @return array |
| 78 | */ |
| 79 | public function get_default(): array { |
| 80 | $ret = array( |
| 81 | self::KEY_FEATURE => 'f', |
| 82 | self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES[1], |
| 83 | self::KEY_SEND_AT => array(), |
| 84 | self::KEY_AVAILABLE_RULES => 31, |
| 85 | self::KEY_DENY_BACKTRACK_ERROR => '1', |
| 86 | ); |
| 87 | |
| 88 | return $ret; |
| 89 | } |
| 90 | |
| 91 | |
| 92 | /** |
| 93 | * 設定定義値取得 |
| 94 | * |
| 95 | * @return array |
| 96 | */ |
| 97 | public function get_constant_settings(): array { |
| 98 | $ret = array( |
| 99 | self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES, |
| 100 | self::KEY_AVAILABLE_RULES => self::RULES_CATEGORY_VALUES, |
| 101 | self::RULES_CATEGORY => array( |
| 102 | self::RULES_CATEGORY_VALUES[0] => self::RULES_CATEGORY_NAMES[0], |
| 103 | self::RULES_CATEGORY_VALUES[1] => self::RULES_CATEGORY_NAMES[1], |
| 104 | self::RULES_CATEGORY_VALUES[2] => self::RULES_CATEGORY_NAMES[2], |
| 105 | self::RULES_CATEGORY_VALUES[3] => self::RULES_CATEGORY_NAMES[3], |
| 106 | self::RULES_CATEGORY_VALUES[4] => self::RULES_CATEGORY_NAMES[4], |
| 107 | ), |
| 108 | ); |
| 109 | return $ret; |
| 110 | } |
| 111 | |
| 112 | |
| 113 | /** |
| 114 | * 設定値取得 |
| 115 | * |
| 116 | * @return array |
| 117 | */ |
| 118 | public function get_settings(): array { |
| 119 | $settings = array(); |
| 120 | $default = $this->get_default(); |
| 121 | |
| 122 | foreach ( $default as $key => $val ) { |
| 123 | $settings[ $key ] = $this->config->get( $key ); |
| 124 | } |
| 125 | |
| 126 | return $settings; |
| 127 | } |
| 128 | |
| 129 | |
| 130 | /** |
| 131 | * 設定値保存 |
| 132 | * |
| 133 | * @param array $settings |
| 134 | * @return void |
| 135 | */ |
| 136 | public function save_settings( $settings ): void { |
| 137 | $default = $this->get_default(); |
| 138 | |
| 139 | foreach ( $default as $key => $val ) { |
| 140 | $this->config->set( $key, $settings[ $key ] ?? '' ); |
| 141 | } |
| 142 | |
| 143 | $this->config->save(); |
| 144 | } |
| 145 | |
| 146 | |
| 147 | /** |
| 148 | * テーブル名取得 |
| 149 | * |
| 150 | * @return string |
| 151 | */ |
| 152 | public function get_table_name(): string { |
| 153 | global $wpdb; |
| 154 | return $wpdb->prefix . self::TABLE_NAME; |
| 155 | } |
| 156 | |
| 157 | |
| 158 | /** |
| 159 | * wafLogテーブルカラム� |
| 160 | 報取得 |
| 161 | * |
| 162 | * @return array |
| 163 | */ |
| 164 | public function get_cloumns(): array { |
| 165 | return self::COLUMNS; |
| 166 | } |
| 167 | |
| 168 | |
| 169 | /** |
| 170 | * テーブル作成 |
| 171 | * |
| 172 | * @return void |
| 173 | */ |
| 174 | public function create_table(): void { |
| 175 | global $wpdb; |
| 176 | $table_name = $this->get_table_name(); |
| 177 | $table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ); |
| 178 | $charset_collate = $wpdb->get_charset_collate(); |
| 179 | |
| 180 | if ( ! is_null( $table ) ) { |
| 181 | $sql = "ALTER TABLE {$table_name} ALTER url DROP DEFAULT"; |
| 182 | $sql2 = "ALTER TABLE {$table_name} ALTER matched DROP DEFAULT"; |
| 183 | $wpdb->query( $sql ); |
| 184 | $wpdb->query( $sql2 ); |
| 185 | |
| 186 | } else { |
| 187 | $sql = "CREATE TABLE {$table_name} ( |
| 188 | id BIGINT( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT, |
| 189 | access_at DATETIME, |
| 190 | attack VARCHAR( 255 ) NOT NULL DEFAULT '', |
| 191 | url VARCHAR( 32767 ) NOT NULL, |
| 192 | matched VARCHAR( 32767 ) NOT NULL, |
| 193 | ip VARCHAR( 39 ) NOT NULL DEFAULT '', |
| 194 | UNIQUE KEY id ( id ) |
| 195 | ) {$charset_collate}"; |
| 196 | |
| 197 | $wpdb->query( $sql ); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | |
| 202 | /** |
| 203 | * 攻撃種別の名前を取得 |
| 204 | * |
| 205 | * @param string $attack |
| 206 | * @return string |
| 207 | */ |
| 208 | public function attack2name( $attack ): string { |
| 209 | $attack_category = $this->get_constant_settings(); |
| 210 | return $attack_category[ self::RULES_CATEGORY ][ $attack ]; |
| 211 | } |
| 212 | |
| 213 | |
| 214 | /** |
| 215 | * ログ登録 |
| 216 | * |
| 217 | * @param array $match_results |
| 218 | * @return void |
| 219 | */ |
| 220 | public function write_log( $match_results ): void { |
| 221 | global $wpdb; |
| 222 | $table_name = $this->get_table_name(); |
| 223 | $max_log = self::MAX_LOG; |
| 224 | |
| 225 | if ( mb_strlen( $match_results['url'], 'UTF-8' ) > 32767 ) { |
| 226 | $match_results['url'] = mb_substr( $match_results['url'], 0, 32767, 'UTF-8' ); |
| 227 | } |
| 228 | |
| 229 | if ( mb_strlen( $match_results['matched'], 'UTF-8' ) > 32767 ) { |
| 230 | $match_results['matched'] = mb_substr( $match_results['matched'], 0, 32767, 'UTF-8' ); |
| 231 | } |
| 232 | |
| 233 | // attack_nameが指定されている場合はそのまま使用、それ以外はattack値から変換 |
| 234 | $attack_name = isset( $match_results['attack_name'] ) |
| 235 | ? $match_results['attack_name'] |
| 236 | : $this->attack2name( $match_results['attack'] ); |
| 237 | |
| 238 | $data = array( |
| 239 | self::COLUMN_ACCESS_AT => $match_results['access_at'], |
| 240 | self::COLUMN_ATTACK => $attack_name, |
| 241 | self::COLUMN_URL => $match_results['url'], |
| 242 | self::COLUMN_MATCHED => $match_results['matched'], |
| 243 | self::COLUMN_IP => $match_results['ip'], |
| 244 | ); |
| 245 | |
| 246 | try { |
| 247 | $wpdb->query( 'START TRANSACTION' ); |
| 248 | |
| 249 | $result = $wpdb->insert( $table_name, $data ); |
| 250 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 251 | throw new Exception( 'Failed to insert WAF log.' ); |
| 252 | } |
| 253 | |
| 254 | $row = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}cloudsecurewp_waf_log ORDER BY id DESC LIMIT 1 OFFSET %d", $max_log ), ARRAY_A ); |
| 255 | |
| 256 | if ( ! empty( $row ?? array() ) ) { |
| 257 | $result = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}cloudsecurewp_waf_log WHERE id <= %d", $row['id'] ) ); |
| 258 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 259 | throw new Exception( 'Failed to delete old WAF logs.' ); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | $wpdb->query( 'COMMIT' ); |
| 264 | } catch ( Exception $e ) { |
| 265 | $wpdb->query( 'ROLLBACK' ); |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | |
| 270 | /** |
| 271 | * ログ取得 |
| 272 | * |
| 273 | * @param string $orderby |
| 274 | * @param string $order |
| 275 | * @param int $per_page |
| 276 | * @param int $offset |
| 277 | * @return array |
| 278 | */ |
| 279 | public function get_block_history( $orderby, $order, $per_page, $offset ): array { |
| 280 | global $wpdb; |
| 281 | $table_name = $this->get_table_name(); |
| 282 | $allowed_orderby = array( 'access_at', 'attack', 'url', 'matched', 'ip' ); |
| 283 | $orderby = in_array( $orderby, $allowed_orderby, true ) ? $orderby : 'access_at'; |
| 284 | $order = ( 'asc' === $order ) ? 'ASC' : 'DESC'; |
| 285 | $sql = $wpdb->prepare( "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d", $per_page, $offset ); |
| 286 | |
| 287 | return array( |
| 288 | $wpdb->get_results( $sql, ARRAY_A ), |
| 289 | $wpdb->get_var( "SELECT count(*) FROM {$table_name}" ), |
| 290 | ); |
| 291 | } |
| 292 | |
| 293 | |
| 294 | /** |
| 295 | * バックトラック� |
| 296 | 過エラーの通知処理 |
| 297 | * |
| 298 | * @param array $results waf_engine から返された結果� |
| 299 | �列 |
| 300 | * @return void |
| 301 | */ |
| 302 | public function block_notice_backtrack( $results ): void { |
| 303 | $match_access_at = strtotime( $results['access_at'] ); |
| 304 | $settings = $this->get_settings(); |
| 305 | $send_at = $settings[ self::KEY_SEND_AT ] ?? array(); |
| 306 | $tmp_send_at = 0; |
| 307 | $throttle_key = 'backtrack_error'; |
| 308 | |
| 309 | if ( ! empty( $send_at ) && is_array( $send_at ) ) { |
| 310 | foreach ( $send_at as $key => $val ) { |
| 311 | if ( $throttle_key === $key ) { |
| 312 | $tmp_send_at = strtotime( $val ); |
| 313 | break; |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | if ( 60 <= $match_access_at - $tmp_send_at || $tmp_send_at === 0 ) { |
| 319 | $subject = 'アクセスをブロックしました [' . $results['access_at'] . ']'; |
| 320 | |
| 321 | $body = $results['access_at'] . ' にWAFの検査で「' . self::BACKTRACK_ATTACK_NAME . "」となったアクセスをブロックしました。\n\n"; |
| 322 | $body .= "詳細はこちらからご確認ください。\n"; |
| 323 | $body .= admin_url( 'admin.php?page=cloudsecurewp_waf&childpage=log' ) . "\n\n"; |
| 324 | $body .= "--\nCloudSecure WP Security\n"; |
| 325 | |
| 326 | $admins = $this->get_admin_users(); |
| 327 | |
| 328 | foreach ( $admins as $admin ) { |
| 329 | $this->wp_send_mail( $admin->user_email, esc_html( $subject ), esc_html( $body ) ); |
| 330 | } |
| 331 | |
| 332 | $send_at[ $throttle_key ] = $results['access_at']; |
| 333 | |
| 334 | $this->config->set( self::KEY_SEND_AT, $send_at ); |
| 335 | $this->config->save(); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | |
| 340 | /** |
| 341 | * ブロック通知処理 |
| 342 | * |
| 343 | * @param array $match_results |
| 344 | * @return void |
| 345 | */ |
| 346 | public function block_notice( $match_results ): void { |
| 347 | $match_access_at = strtotime( $match_results['access_at'] ); |
| 348 | $settings = $this->get_settings(); |
| 349 | $send_at = $settings[ self::KEY_SEND_AT ] ?? array(); |
| 350 | $tmp_send_at = 0; |
| 351 | |
| 352 | if ( ! empty( $send_at ) && is_array( $send_at ) ) { |
| 353 | foreach ( $send_at as $key => $val ) { |
| 354 | if ( $match_results['attack'] === $key ) { |
| 355 | $tmp_send_at = strtotime( $val ); |
| 356 | break; |
| 357 | } |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | if ( 60 <= $match_access_at - $tmp_send_at || $tmp_send_at === 0 ) { |
| 362 | $subject = 'アクセスをブロックしました [' . $match_results['access_at'] . ']'; |
| 363 | |
| 364 | $body = $match_results['access_at'] . ' に「' . $this->attack2name( $match_results['attack'] ) . "」の攻撃をブロックしました。\n\n"; |
| 365 | $body .= "詳細はこちらからご確認ください。\n"; |
| 366 | $body .= admin_url( 'admin.php?page=cloudsecurewp_waf&childpage=log' ) . "\n\n"; |
| 367 | $body .= "--\nCloudSecure WP Security\n"; |
| 368 | |
| 369 | $admins = $this->get_admin_users(); |
| 370 | |
| 371 | foreach ( $admins as $admin ) { |
| 372 | $this->wp_send_mail( $admin->user_email, esc_html( $subject ), esc_html( $body ) ); |
| 373 | } |
| 374 | |
| 375 | $send_at[ $match_results['attack'] ] = $match_results['access_at']; |
| 376 | |
| 377 | $this->config->set( self::KEY_SEND_AT, $send_at ); |
| 378 | $this->config->save(); |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | |
| 383 | public function waf() :void { |
| 384 | $settings = $this->get_settings(); |
| 385 | $waf_rules = $this->waf_rules->get_waf_rules(); |
| 386 | $locationmatch_rules = $this->waf_rules->get_locationmatch_rules(); |
| 387 | $remove_rules = array( |
| 388 | 'ajax_editor' => array('950001', '950901', '950004', '950904', '950006', '950906', '950007', '950907', '950908', '950013', '950019', 'm340095' ), |
| 389 | 'ajax_customize' => array( '950904', '950906', '950004', '950001', '950007' ), |
| 390 | 'rest_api' => array( '950004', '950001', '950007', '950006' ), |
| 391 | 'rest_api_search' => array( '950007', '950904', '950001', '950901', '950906', '950908', '950004', '950006', '950907', '950013', '950019', 'm340095' ), |
| 392 | 'comment' => array( '950004' ), |
| 393 | 'cocoon' => array( '950004' ), |
| 394 | 'emanon' => array( '950004', '950001', '950007' ), |
| 395 | 'vkexunit' => array( '950004', '950001', '950007' ), |
| 396 | 'nishiki' => array( '950004', '950001', '950007' ), |
| 397 | 'swell' => array( '950004', '950001', '950007' ), |
| 398 | 'woocommerce' => array( '959006' ), |
| 399 | 'rename_login_page' => array( |
| 400 | 'login_page_name' => $this->config->get( 'rename_login_page' ) === 't' ? $this->config->get( 'rename_login_page_name' ) : '', |
| 401 | 'rule_ids' => array( |
| 402 | '950007', '959007', '950904', '959904', '950001', '959001', |
| 403 | '950906', '959906', '950908', '959908', '950004', '959004', |
| 404 | '950006', '959006', '950907', '959907', '950013', '959013', |
| 405 | ), |
| 406 | ), |
| 407 | ); |
| 408 | |
| 409 | // 未設定(移行前等で空文字)の場合は安� |
| 410 | �側の '1'(遮断)にフォールバックする |
| 411 | $deny_on_backtrack_error = $settings[ self::KEY_DENY_BACKTRACK_ERROR ]; |
| 412 | if ( '' === $deny_on_backtrack_error ) { |
| 413 | $deny_on_backtrack_error = '1'; |
| 414 | } |
| 415 | |
| 416 | $results = $this->waf_engine( $waf_rules, $locationmatch_rules, $settings[ self::KEY_AVAILABLE_RULES ], $remove_rules, $deny_on_backtrack_error ); |
| 417 | |
| 418 | if ( $results['is_deny'] && $results['is_write_log'] ) { |
| 419 | if ( ! empty( $results['is_backtrack_error'] ) ) { |
| 420 | $results['attack_name'] = self::BACKTRACK_ATTACK_NAME; |
| 421 | } |
| 422 | $this->write_log( $results ); |
| 423 | |
| 424 | if ( self::SEND_ADMIN_MAIL_VALUES[1] === (int) $settings[ self::KEY_SEND_ADMIN_MAIL ] ) { |
| 425 | if ( ! empty( $results['is_backtrack_error'] ) ) { |
| 426 | $this->block_notice_backtrack( $results ); |
| 427 | } else { |
| 428 | $this->block_notice( $results ); |
| 429 | } |
| 430 | } |
| 431 | |
| 432 | $this->page403(); |
| 433 | exit; |
| 434 | |
| 435 | } elseif ( $results['is_write_log'] ) { |
| 436 | if ( ! empty( $results['is_backtrack_error'] ) ) { |
| 437 | $results['attack_name'] = self::BACKTRACK_ATTACK_NAME; |
| 438 | } |
| 439 | $this->write_log( $results ); |
| 440 | } |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * 有効化 |
| 445 | * |
| 446 | * @return void |
| 447 | */ |
| 448 | public function activate(): void { |
| 449 | $this->save_settings( $this->get_default() ); |
| 450 | $this->create_table(); |
| 451 | } |
| 452 | |
| 453 | |
| 454 | /** |
| 455 | * v1.4.12: 検査上限� |
| 456 | 過時の扱いのデフォルト値を移行する |
| 457 | * WAFの有効・無効にかかわらず1(遮断)を設定する |
| 458 | * |
| 459 | * @return void |
| 460 | */ |
| 461 | public function migrate_waf_backtrack_error_default(): void { |
| 462 | $this->config->set( self::KEY_DENY_BACKTRACK_ERROR, '1' ); |
| 463 | $this->config->save(); |
| 464 | } |
| 465 | |
| 466 | |
| 467 | /** |
| 468 | * 無効化 |
| 469 | * |
| 470 | * @return void |
| 471 | */ |
| 472 | public function deactivate(): void { |
| 473 | $this->config->set( self::KEY_FEATURE, 'f' ); |
| 474 | $this->config->save(); |
| 475 | } |
| 476 | } |
| 477 |