admin
2 months ago
cli
2 months ago
lib
3 months ago
captcha.php
3 months ago
cloudsecure-wp.php
2 months ago
common.php
2 months ago
config.php
2 years ago
disable-access-system-file.php
4 months ago
disable-author-query.php
2 years ago
disable-login.php
2 months ago
disable-restapi.php
2 months ago
disable-xmlrpc.php
1 year ago
htaccess.php
3 months ago
login-log.php
2 months ago
login-notification.php
1 year ago
rename-login-page.php
3 months ago
restrict-admin-page.php
3 months ago
server-error-notification.php
2 months ago
two-factor-authentication.php
2 months ago
unify-messages.php
2 years ago
update-notice.php
7 months ago
waf-engine.php
3 months ago
waf.php
2 months ago
waf.php
383 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 RULES_CATEGORY = self::KEY_FEATURE . '_rules_category'; |
| 14 | private const RULES_CATEGORY_VALUES = array( 1, 2, 4, 8, 16 ); |
| 15 | private const RULES_CATEGORY_NAMES = array( |
| 16 | 'SQLインジェクション', |
| 17 | 'クロスサイトスクリプティング', |
| 18 | 'OSコマンドインジェクション', |
| 19 | 'コードインジェクション', |
| 20 | 'メールヘッダインジェクション', |
| 21 | ); |
| 22 | private const TABLE_NAME = 'cloudsecurewp_waf_log'; |
| 23 | private const COLUMN_ID = 'id'; |
| 24 | private const COLUMN_ACCESS_AT = 'access_at'; |
| 25 | private const COLUMN_ATTACK = 'attack'; |
| 26 | private const COLUMN_URL = 'url'; |
| 27 | private const COLUMN_MATCHED = 'matched'; |
| 28 | private const COLUMN_IP = 'ip'; |
| 29 | private const COLUMNS = array( |
| 30 | self::COLUMN_ID => 'ID', |
| 31 | self::COLUMN_ACCESS_AT => '日時', |
| 32 | self::COLUMN_ATTACK => '攻撃種別', |
| 33 | self::COLUMN_URL => '検知したページのURL', |
| 34 | self::COLUMN_MATCHED => 'マッチした文字列', |
| 35 | self::COLUMN_IP => '攻撃� |
| 36 | �IPアドレス', |
| 37 | ); |
| 38 | private const MAX_LOG = 10000; |
| 39 | private $config; |
| 40 | private $waf_rules; |
| 41 | |
| 42 | |
| 43 | |
| 44 | function __construct( array $info, CloudSecureWP_Config $config ) { |
| 45 | parent::__construct( $info ); |
| 46 | $this->config = $config; |
| 47 | $this->waf_rules = new CloudSecureWP_Waf_Rules(); |
| 48 | } |
| 49 | |
| 50 | |
| 51 | /** |
| 52 | * 機能毎のKEY取得 |
| 53 | * |
| 54 | * @return string |
| 55 | */ |
| 56 | public function get_feature_key(): string { |
| 57 | return self::KEY_FEATURE; |
| 58 | } |
| 59 | |
| 60 | |
| 61 | /** |
| 62 | * 有効無効判定 |
| 63 | * |
| 64 | * @return bool |
| 65 | */ |
| 66 | public function is_enabled(): bool { |
| 67 | return $this->config->get( $this->get_feature_key() ) === 't' ? true : false; |
| 68 | } |
| 69 | |
| 70 | |
| 71 | /** |
| 72 | * 初期設定値取得 |
| 73 | * |
| 74 | * @return array |
| 75 | */ |
| 76 | public function get_default(): array { |
| 77 | $ret = array( |
| 78 | self::KEY_FEATURE => 'f', |
| 79 | self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES[1], |
| 80 | self::KEY_SEND_AT => array(), |
| 81 | self::KEY_AVAILABLE_RULES => 31, |
| 82 | ); |
| 83 | |
| 84 | return $ret; |
| 85 | } |
| 86 | |
| 87 | |
| 88 | /** |
| 89 | * 設定定義値取得 |
| 90 | * |
| 91 | * @return array |
| 92 | */ |
| 93 | public function get_constant_settings(): array { |
| 94 | $ret = array( |
| 95 | self::KEY_SEND_ADMIN_MAIL => self::SEND_ADMIN_MAIL_VALUES, |
| 96 | self::KEY_AVAILABLE_RULES => self::RULES_CATEGORY_VALUES, |
| 97 | self::RULES_CATEGORY => array( |
| 98 | self::RULES_CATEGORY_VALUES[0] => self::RULES_CATEGORY_NAMES[0], |
| 99 | self::RULES_CATEGORY_VALUES[1] => self::RULES_CATEGORY_NAMES[1], |
| 100 | self::RULES_CATEGORY_VALUES[2] => self::RULES_CATEGORY_NAMES[2], |
| 101 | self::RULES_CATEGORY_VALUES[3] => self::RULES_CATEGORY_NAMES[3], |
| 102 | self::RULES_CATEGORY_VALUES[4] => self::RULES_CATEGORY_NAMES[4], |
| 103 | ), |
| 104 | ); |
| 105 | return $ret; |
| 106 | } |
| 107 | |
| 108 | |
| 109 | /** |
| 110 | * 設定値取得 |
| 111 | * |
| 112 | * @return array |
| 113 | */ |
| 114 | public function get_settings(): array { |
| 115 | $settings = array(); |
| 116 | $default = $this->get_default(); |
| 117 | |
| 118 | foreach ( $default as $key => $val ) { |
| 119 | $settings[ $key ] = $this->config->get( $key ); |
| 120 | } |
| 121 | |
| 122 | return $settings; |
| 123 | } |
| 124 | |
| 125 | |
| 126 | /** |
| 127 | * 設定値保存 |
| 128 | * |
| 129 | * @param array $settings |
| 130 | * @return void |
| 131 | */ |
| 132 | public function save_settings( $settings ): void { |
| 133 | $default = $this->get_default(); |
| 134 | |
| 135 | foreach ( $default as $key => $val ) { |
| 136 | $this->config->set( $key, $settings[ $key ] ?? '' ); |
| 137 | } |
| 138 | |
| 139 | $this->config->save(); |
| 140 | } |
| 141 | |
| 142 | |
| 143 | /** |
| 144 | * テーブル名取得 |
| 145 | * |
| 146 | * @return string |
| 147 | */ |
| 148 | public function get_table_name(): string { |
| 149 | global $wpdb; |
| 150 | return $wpdb->prefix . self::TABLE_NAME; |
| 151 | } |
| 152 | |
| 153 | |
| 154 | /** |
| 155 | * wafLogテーブルカラム� |
| 156 | 報取得 |
| 157 | * |
| 158 | * @return array |
| 159 | */ |
| 160 | public function get_cloumns(): array { |
| 161 | return self::COLUMNS; |
| 162 | } |
| 163 | |
| 164 | |
| 165 | /** |
| 166 | * テーブル作成 |
| 167 | * |
| 168 | * @return void |
| 169 | */ |
| 170 | public function create_table(): void { |
| 171 | global $wpdb; |
| 172 | $table_name = $this->get_table_name(); |
| 173 | $table = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table_name ) ) ); |
| 174 | $charset_collate = $wpdb->get_charset_collate(); |
| 175 | |
| 176 | if ( ! is_null( $table ) ) { |
| 177 | $sql = "ALTER TABLE {$table_name} ALTER url DROP DEFAULT"; |
| 178 | $sql2 = "ALTER TABLE {$table_name} ALTER matched DROP DEFAULT"; |
| 179 | $wpdb->query( $sql ); |
| 180 | $wpdb->query( $sql2 ); |
| 181 | |
| 182 | } else { |
| 183 | $sql = "CREATE TABLE {$table_name} ( |
| 184 | id BIGINT( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT, |
| 185 | access_at DATETIME, |
| 186 | attack VARCHAR( 255 ) NOT NULL DEFAULT '', |
| 187 | url VARCHAR( 32767 ) NOT NULL, |
| 188 | matched VARCHAR( 32767 ) NOT NULL, |
| 189 | ip VARCHAR( 39 ) NOT NULL DEFAULT '', |
| 190 | UNIQUE KEY id ( id ) |
| 191 | ) {$charset_collate}"; |
| 192 | |
| 193 | $wpdb->query( $sql ); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | |
| 198 | /** |
| 199 | * 攻撃種別の名前を取得 |
| 200 | * |
| 201 | * @param string $attack |
| 202 | * @return string |
| 203 | */ |
| 204 | public function attack2name( $attack ): string { |
| 205 | $attack_category = $this->get_constant_settings(); |
| 206 | return $attack_category[ self::RULES_CATEGORY ][ $attack ]; |
| 207 | } |
| 208 | |
| 209 | |
| 210 | /** |
| 211 | * ログ登録 |
| 212 | * |
| 213 | * @param array $match_results |
| 214 | * @return void |
| 215 | */ |
| 216 | public function write_log( $match_results ): void { |
| 217 | global $wpdb; |
| 218 | $table_name = $this->get_table_name(); |
| 219 | $max_log = self::MAX_LOG; |
| 220 | |
| 221 | if ( mb_strlen( $match_results['url'], 'UTF-8' ) > 32767 ) { |
| 222 | $match_results['url'] = mb_substr( $match_results['url'], 0, 32767, 'UTF-8' ); |
| 223 | } |
| 224 | |
| 225 | if ( mb_strlen( $match_results['matched'], 'UTF-8' ) > 32767 ) { |
| 226 | $match_results['matched'] = mb_substr( $match_results['matched'], 0, 32767, 'UTF-8' ); |
| 227 | } |
| 228 | |
| 229 | $data = array( |
| 230 | self::COLUMN_ACCESS_AT => $match_results['access_at'], |
| 231 | self::COLUMN_ATTACK => $this->attack2name( $match_results['attack'] ), |
| 232 | self::COLUMN_URL => $match_results['url'], |
| 233 | self::COLUMN_MATCHED => $match_results['matched'], |
| 234 | self::COLUMN_IP => $match_results['ip'], |
| 235 | ); |
| 236 | |
| 237 | try { |
| 238 | $wpdb->query( 'START TRANSACTION' ); |
| 239 | |
| 240 | $result = $wpdb->insert( $table_name, $data ); |
| 241 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 242 | throw new Exception( 'Failed to insert WAF log.' ); |
| 243 | } |
| 244 | |
| 245 | $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 ); |
| 246 | |
| 247 | if ( ! empty( $row ?? array() ) ) { |
| 248 | $result = $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}cloudsecurewp_waf_log WHERE id <= %d", $row['id'] ) ); |
| 249 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 250 | throw new Exception( 'Failed to delete old WAF logs.' ); |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | $wpdb->query( 'COMMIT' ); |
| 255 | } catch ( Exception $e ) { |
| 256 | $wpdb->query( 'ROLLBACK' ); |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | |
| 261 | /** |
| 262 | * ログ取得 |
| 263 | * |
| 264 | * @param string $orderby |
| 265 | * @param string $order |
| 266 | * @param int $per_page |
| 267 | * @param int $offset |
| 268 | * @return array |
| 269 | */ |
| 270 | public function get_block_history( $orderby, $order, $per_page, $offset ): array { |
| 271 | global $wpdb; |
| 272 | $table_name = $this->get_table_name(); |
| 273 | $allowed_orderby = array( 'access_at', 'attack', 'url', 'matched', 'ip' ); |
| 274 | $orderby = in_array( $orderby, $allowed_orderby, true ) ? $orderby : 'access_at'; |
| 275 | $order = ( 'asc' === $order ) ? 'ASC' : 'DESC'; |
| 276 | $sql = "SELECT * FROM {$table_name} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; |
| 277 | |
| 278 | return array( |
| 279 | $wpdb->get_results( $wpdb->prepare( $sql, $per_page, $offset ), ARRAY_A ), |
| 280 | $wpdb->get_var( "SELECT count(*) FROM {$table_name}" ), |
| 281 | ); |
| 282 | } |
| 283 | |
| 284 | |
| 285 | /** |
| 286 | * ブロック通知処理 |
| 287 | * |
| 288 | * @param array $match_results |
| 289 | * @return void |
| 290 | */ |
| 291 | public function brock_notice( $match_results ): void { |
| 292 | $match_access_at = strtotime( $match_results['access_at'] ); |
| 293 | $settings = $this->get_settings(); |
| 294 | $send_at = $settings[ self::KEY_SEND_AT ] ?? array(); |
| 295 | $tmp_send_at = 0; |
| 296 | |
| 297 | if ( ! empty( $send_at ) && is_array( $send_at ) ) { |
| 298 | foreach ( $send_at as $key => $val ) { |
| 299 | if ( $match_results['attack'] === $key ) { |
| 300 | $tmp_send_at = strtotime( $val ); |
| 301 | break; |
| 302 | } |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | if ( 60 <= $match_access_at - $tmp_send_at || $tmp_send_at === 0 ) { |
| 307 | $subject = 'アクセスをブロックしました [' . $match_results['access_at'] . ']'; |
| 308 | |
| 309 | $body = $match_results['access_at'] . ' に「' . $this->attack2name( $match_results['attack'] ) . "」の攻撃をブロックしました。\n\n"; |
| 310 | $body .= "詳細はこちらからご確認ください。\n"; |
| 311 | $body .= admin_url( 'admin.php?page=cloudsecurewp_waf&childpage=log' ) . "\n\n"; |
| 312 | $body .= "--\nCloudSecure WP Security\n"; |
| 313 | |
| 314 | $admins = $this->get_admin_users(); |
| 315 | |
| 316 | foreach ( $admins as $admin ) { |
| 317 | $this->wp_send_mail( $admin->user_email, esc_html( $subject ), esc_html( $body ) ); |
| 318 | } |
| 319 | |
| 320 | $send_at[ $match_results['attack'] ] = $match_results['access_at']; |
| 321 | |
| 322 | $this->config->set( self::KEY_SEND_AT, $send_at ); |
| 323 | $this->config->save(); |
| 324 | } |
| 325 | } |
| 326 | |
| 327 | |
| 328 | public function waf() :void { |
| 329 | $settings = $this->get_settings(); |
| 330 | $waf_rules = $this->waf_rules->get_waf_rules(); |
| 331 | $locationmatch_rules = $this->waf_rules->get_locationmatch_rules(); |
| 332 | $remove_rules = array( |
| 333 | 'ajax_editor' => array('950001', '950901', '950004', '950904', '950006', '950906', '950007', '950907', '950908', '950013', '950019', 'm340095' ), |
| 334 | 'ajax_customize' => array( '950904', '950906', '950004', '950001', '950007' ), |
| 335 | 'rest_api' => array( '950004', '950001', '950007', '950006' ), |
| 336 | 'comment' => array( '950004' ), |
| 337 | 'cocoon' => array( '950004' ), |
| 338 | 'emanon' => array( '950004', '950001', '950007' ), |
| 339 | 'vkexunit' => array( '950004', '950001', '950007' ), |
| 340 | 'nishiki' => array( '950004', '950001', '950007' ), |
| 341 | 'swell' => array( '950004', '950001', '950007' ), |
| 342 | 'woocommerce' => array( '959006' ), |
| 343 | ); |
| 344 | |
| 345 | $results = $this->waf_engine( $waf_rules, $locationmatch_rules, $settings[ self::KEY_AVAILABLE_RULES ], $remove_rules ); |
| 346 | |
| 347 | if ( $results['is_deny'] && $results['is_write_log'] ) { |
| 348 | $this->write_log( $results ); |
| 349 | |
| 350 | if ( self::SEND_ADMIN_MAIL_VALUES[1] === (int) $settings[ self::KEY_SEND_ADMIN_MAIL ] ) { |
| 351 | $this->brock_notice( $results ); |
| 352 | } |
| 353 | |
| 354 | $this->page403(); |
| 355 | exit; |
| 356 | |
| 357 | } elseif ( $results['is_write_log'] ) { |
| 358 | $this->write_log( $results ); |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | /** |
| 363 | * 有効化 |
| 364 | * |
| 365 | * @return void |
| 366 | */ |
| 367 | public function activate(): void { |
| 368 | $this->save_settings( $this->get_default() ); |
| 369 | $this->create_table(); |
| 370 | } |
| 371 | |
| 372 | |
| 373 | /** |
| 374 | * 無効化 |
| 375 | * |
| 376 | * @return void |
| 377 | */ |
| 378 | public function deactivate(): void { |
| 379 | $this->config->set( self::KEY_FEATURE, 'f' ); |
| 380 | $this->config->save(); |
| 381 | } |
| 382 | } |
| 383 |