PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.9
CloudSecure WP Security v1.4.9
1.4.10 1.4.9 trunk 0.9.0 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.19 1.3.2 1.3.20 1.3.21 1.3.22 1.3.23 1.3.24 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8
cloudsecure-wp-security / modules / waf-engine.php
cloudsecure-wp-security / modules Last commit date
admin 1 month ago cli 2 months ago lib 3 months ago captcha.php 3 months ago cloudsecure-wp.php 1 month 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 1 month ago unify-messages.php 2 years ago update-notice.php 7 months ago waf-engine.php 1 month ago waf.php 1 month ago
waf-engine.php
1208 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class CloudSecureWP_Waf_Engine extends CloudSecureWP_Common {
8 protected const VARIABLE_ARGS = 'args';
9 protected const VARIABLE_ARGS_NAMES = 'args_names';
10 protected const VARIABLE_REQUEST_COOKIES = 'request_cookies';
11 protected const VARIABLE_REQUEST_COOKIES_NAMES = 'request_cookies_names';
12 protected const VARIABLE_REQUEST_HEADERS = 'request_headers';
13 protected const VARIABLE_REQUEST_FILENAME = 'request_filename';
14 protected const VARIABLE_XML = 'xml';
15 private $parsed_xml;
16
17
18
19 function __construct( array $info ) {
20 parent::__construct( $info );
21
22 }
23
24
25 /**
26 * XML パース時のコールバック関数
27 *
28 * @return void
29 */
30 public function char( $parser, $data ): void {
31 $this->parsed_xml .= $data;
32 }
33
34
35 /**
36 * XML パース
37 *
38 * @return string
39 */
40 public function xml_parser(): string {
41 $request_body = file_get_contents( 'php://input' );
42 $this->parsed_xml = '';
43
44 if ( ! empty( $request_body ) ) {
45 $parser = xml_parser_create();
46
47 xml_set_object( $parser, $this );
48 xml_set_character_data_handler( $parser, 'char' );
49
50 if ( ! xml_parse( $parser, $request_body ) ) {
51 $this->parsed_xml .= 'xml_parse_failed';
52 };
53
54 xml_parser_free( $parser );
55 }
56
57 return $this->parsed_xml;
58 }
59
60
61 /**
62 * ARGS、ARGS_NAMES 用 クエリ文字列、POSTデータ取得
63 *
64 * @return array
65 */
66 public function get_request_args_data(): array {
67 $args = array();
68
69 if ( ! empty( $_GET ) ) {
70 $args = $_GET;
71 }
72
73 if ( ! empty( $_POST ) ) {
74 $args = array_merge( $args, $_POST );
75
76 } else {
77 $post_data = file_get_contents( 'php://input' );
78
79 if ( ! empty( $post_data ) ) {
80 $json_decoded_post_data = json_decode( $post_data, true );
81
82 // nullでなければjsonとなる
83 if ( is_array( $json_decoded_post_data ) ) {
84 $args = array_merge( $args, $json_decoded_post_data );
85 }
86 }
87 }
88
89 return $args;
90 }
91
92
93 /**
94 * Cookie�
95 報取得
96 *
97 * @return array
98 */
99 public function get_request_cookies(): array {
100 if ( ! empty( $_COOKIE ) ) {
101 return $_COOKIE;
102 } else {
103 return array();
104 }
105 }
106
107
108 /**
109 * リクエスト�
110 報取得
111 * 取得・パースできなかったものに関しては空白で返す
112 *
113 * @return array
114 */
115 public function get_request_items(): array {
116 $request_items = array(
117 'access_at' => current_time( 'mysql' ),
118 'ip' => $this->get_client_ip(),
119 self::VARIABLE_XML => $this->xml_parser(),
120 self::VARIABLE_REQUEST_FILENAME => $this->get_request_uri(),
121 self::VARIABLE_REQUEST_HEADERS => $this->get_http_request_headers(),
122 self::VARIABLE_ARGS => $this->get_request_args_data(),
123 self::VARIABLE_REQUEST_COOKIES => $this->get_request_cookies(),
124 );
125
126 return $request_items;
127 }
128
129
130 /**
131 * ルールの設定を参考に1つのリクエスト�
132 報に対して複数の変換を行う処理
133 *
134 * @param array $transformations
135 * @param string $request_item
136 * @return string
137 */
138 public function transform( $transformations, $request_item ): string {
139 $converted_request_item = $request_item;
140
141 foreach ( $transformations as $transformation ) {
142 switch ( $transformation ) {
143 case 'htmlentitydecode':
144 $converted_request_item = html_entity_decode( $converted_request_item, ENT_QUOTES, 'utf-8' );
145 break;
146 case 'lowercase':
147 $converted_request_item = strtolower( $converted_request_item );
148 break;
149 case 'replacecomments':
150 $converted_request_item = preg_replace( '/(\/\*.*?\*\/|\/\*.*(?!\*\/).*)/s', ' ', $converted_request_item );
151 break;
152 case 'compresswhitespace':
153 $converted_request_item = preg_replace( '/(\s|\xa0)/s', ' ', $converted_request_item );
154 $converted_request_item = preg_replace( '/\s{2,}/s', ' ', $converted_request_item );
155 break;
156 default:
157 $converted_request_item = '変換に失敗しました';
158 break 2;
159 }
160 }
161
162 return $converted_request_item;
163 }
164
165
166 /**
167 * LocationMatch設定によるルールのスキップ用関数
168 *
169 * @param array $locationmatch_rules
170 * @param string $request_uri
171 * @return array
172 */
173 public function locationmatch_remove_rules( $locationmatch_rules, $request_uri ): array {
174 $locationmatch_removed_rule_ids = array();
175
176 foreach ( $locationmatch_rules as $locationmatch_rule ) {
177 if ( preg_match( '/' . $locationmatch_rule['path'] . '/', $request_uri ) ) {
178 $locationmatch_removed_rule_ids = array_merge( $locationmatch_removed_rule_ids, $locationmatch_rule['remove_rule_ids'] );
179 }
180 }
181
182 $locationmatch_removed_rule_ids = array_unique( $locationmatch_removed_rule_ids );
183
184 return $locationmatch_removed_rule_ids;
185 }
186
187
188 /**
189 * skip表記の存在確認
190 *
191 * @param int $skip
192 * @return int
193 */
194 public function check_skip( $skip ): int {
195 if ( $skip !== 0 ) {
196 return $skip;
197 } else {
198 return 0;
199 }
200 }
201
202
203 /**
204 * skipafter表記の存在確認
205 *
206 * @param string $skipafter
207 * @return string
208 */
209 public function check_skipafter( $skipafter ): string {
210 if ( ! empty( $skipafter ) ) {
211 return $skipafter;
212 } else {
213 return '';
214 }
215 }
216
217
218 /**
219 * skipが有効か判定
220 *
221 * @param int $skip
222 * @return bool
223 */
224 public function is_skip_enabled( $skip ): bool {
225 if ( 0 < $skip ) {
226 return true;
227 } else {
228 return false;
229 }
230 }
231
232
233 /**
234 * skipafterが有効か判定
235 *
236 * @param string $skipafter
237 * @return bool
238 */
239 public function is_skipafter_enabled( $skipafter ): bool {
240 if ( ! empty( $skipafter ) ) {
241 return true;
242 } else {
243 return false;
244 }
245 }
246
247 /**
248 * ルールが終了したか判定
249 *
250 * @param string $chain
251 * @param string $skip
252 * @param string $skipafter
253 * @return bool
254 */
255 public function is_rule_finished( $chain, $skip, $skipafter ): bool {
256 if ( ! empty( $chain ) || $this->is_skip_enabled( $skip ) || $this->is_skipafter_enabled( $skipafter ) ) {
257 return false;
258 } else {
259 return true;
260 }
261 }
262
263
264 /**
265 * ルール�
266 に除外の表記があるか確認
267 *
268 * @param array $remove_variables
269 * @param string $variable
270 * @param string $request_item
271 * @return bool
272 */
273 public function is_remove_variables( $remove_variables, $variable, $request_item ): bool {
274 if ( ! empty( $remove_variables ) ) {
275 foreach ( $remove_variables as $remove_variable => $remove_values ) {
276 if ( $remove_variable === $variable ) {
277 foreach ( $remove_values as $remove_value ) {
278
279 // REQUEST_HEADERSの場合は大文字小文字関係なく判定する
280 if ( $variable === self::VARIABLE_REQUEST_HEADERS ) {
281 if ( preg_match( '/' . $remove_value . '/i', $request_item ) ) {
282 return true;
283 }
284 }
285
286 if ( preg_match( '/^\/.+\/$/', $remove_value ) ) {
287 // 除外の値が部分一致(/で囲まれているもの)の場合
288 if ( preg_match( $remove_value . 's', $request_item ) ) {
289 return true;
290 }
291 } else {
292 // 除外の値が完�
293 �一致の場合
294 if ( $remove_value === $request_item ) {
295 return true;
296 }
297 }
298 }
299 }
300 }
301 }
302
303 return false;
304 }
305
306 /**
307 * マッチした結果を�
308 �列に格納
309 *
310 * @param array $rule
311 * @param array $request_items
312 * @param string $variable
313 * @param string $match_string
314 * @return array
315 */
316 public function get_match_results( $rule, $request_items, $variable, $match_string ): array {
317
318 if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
319 $complete_url = ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' )
320 . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) )
321 . esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
322 } else {
323 $complete_url = '';
324 }
325
326 $match_results = array(
327 'id' => $rule['id'],
328 'attack' => $rule['attack'],
329 'variable' => $variable,
330 'matched' => $match_string,
331 'ip' => $request_items['ip'],
332 'access_at' => $request_items['access_at'],
333 'url' => $complete_url,
334 );
335
336 return $match_results;
337 }
338
339
340 /**
341 * chain_itemsの取得
342 *
343 * @param array $rule
344 * @return array
345 */
346 public function get_chain_items( $rule ): array {
347 $chain_items = array(
348 'id' => $rule['id'],
349 'attack' => $rule['attack'],
350 'skip' => $rule['skip'],
351 'skipafter' => $rule['skipafter'],
352 );
353
354 return $chain_items;
355 }
356
357
358 /**
359 * マッチしたルールの設定、結果を取得
360 *
361 * @param array $waf_rule
362 * @param array $request_items
363 * @param string $variable
364 * @param array $chain_items
365 * @param array $match_string
366 * @return array
367 */
368 public function get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $match_string ): array {
369 $results = array(
370 'is_matched' => true,
371 'chain_items' => array(),
372 'skip' => 0,
373 'skipafter' => '',
374 'match_results' => array(),
375 );
376
377 // 前ルールからchainの設定を引き継いでいるか確認
378 if ( ! empty( $chain_items ) ) {
379 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
380 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
381 $results['is_matched'] = false;
382
383 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $chain_items['skip'], $chain_items['skipafter'] ) ) {
384 $results['match_results'] = $this->get_match_results( $chain_items, $request_items, $variable, $match_string );
385
386 } elseif ( $waf_rule['chain'] ) {
387 // 現在のルールにchainの設定がある場合
388 $results['chain_items'] = $chain_items;
389
390 } else {
391 $results['skip'] = $this->check_skip( $chain_items['skip'] );
392 $results['skipafter'] = $this->check_skipafter( $chain_items['skipafter'] );
393 }
394 } else {
395 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
396 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
397 $results['is_matched'] = false;
398
399 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
400 $results['match_results'] = $this->get_match_results( $waf_rule, $request_items, $variable, $match_string );
401
402 } elseif ( $waf_rule['chain'] ) {
403 // 現在のルールにchainの設定がある場合
404 $results['chain_items'] = $this->get_chain_items( $waf_rule );
405
406 } else {
407 $results['skip'] = $this->check_skip( $waf_rule['skip'] );
408 $results['skipafter'] = $this->check_skipafter( $waf_rule['skipafter'] );
409 }
410 }
411 return $results;
412 }
413
414 /**
415 * ネストした�
416 �列をパース
417 *
418 * @param string $name
419 * @param array $array
420 */
421 public function parse_array( $name, $array ) {
422 $parsed_array = array();
423
424 foreach ( $array as $key => $val ) {
425 if ( is_array( $val ) ) {
426 $tmp_name = $name . '[' . $key . ']';
427 $tmp_parsed_array = $this->parse_array( $tmp_name, $val );
428 $parsed_array = array_merge( $parsed_array, $tmp_parsed_array );
429
430 } else {
431 $parsed_array_key = $name . '[' . $key . ']';
432 $parsed_array[ $parsed_array_key ] = $val ?? '';
433 }
434 }
435 return $parsed_array;
436 }
437
438
439 /**
440 * リクエスト�
441 報�
442 �列を使用するルール判定
443 *
444 * @param array $waf_rule
445 * @param array $request_items
446 * @param string $variable
447 * @param array $chain_items
448 * @return array
449 */
450 public function check_request_item_array( $waf_rule, $request_items, $variable, $chain_items ) {
451 $results = array(
452 'is_matched' => false,
453 'chain_items' => array(),
454 'skip' => 0,
455 'skipafter' => '',
456 'match_results' => array(),
457 );
458
459 $get_request_item_variable = preg_replace( '/_names/', '', $variable );
460
461 if ( ! isset( $get_request_item_variable ) ) {
462 return $results;
463 }
464
465 foreach ( $request_items[ $get_request_item_variable ] as $key => $val ) {
466
467 if ( ! isset( $val ) ) {
468 $val = '';
469 }
470
471 switch ( $variable ) {
472 case self::VARIABLE_ARGS:
473 case self::VARIABLE_REQUEST_COOKIES:
474 case self::VARIABLE_REQUEST_HEADERS:
475 if ( is_array( $val ) ) {
476 $parsed_vals = $this->parse_array( $key, $val );
477
478 foreach ( $parsed_vals as $key => $val ) {
479 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
480
481 if ( $checked_request_item['is_matched'] ) {
482 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
483 break;
484 }
485 }
486 } else {
487 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
488 }
489
490 break;
491
492 case self::VARIABLE_ARGS_NAMES:
493 case self::VARIABLE_REQUEST_COOKIES_NAMES:
494 $checked_request_item = $this->check_request_item_key( $waf_rule, $key, $variable );
495 break;
496 }
497
498 if ( $checked_request_item['is_matched'] ) {
499 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
500
501 break;
502 }
503 }
504 return $results;
505 }
506
507
508
509 /**
510 * リクエスト�
511 報�
512 �列のうちvalueの値を使用するルール判定
513 *
514 * @param array $waf_rule
515 * @param string $request_items_key
516 * @param string $request_items_value
517 * @param string $variable
518 * @return array
519 */
520 public function check_request_item_value( $waf_rule, $request_items_key, $request_items_value, $variable ): array {
521 $results = array(
522 'is_matched' => false,
523 'match_string' => '',
524 );
525 $matches = array();
526
527 // リクエスト�
528 報�
529 �列のkeyとvalueの変換
530 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
531 $tmp_val = $this->transform( $waf_rule['transformations'], $request_items_value );
532
533 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_val, $matches ) ) {
534 if ( ! empty( $matches ) ) {
535 if ( false === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
536 // 除外の設定が無ければマッチしたと判定
537 $results['is_matched'] = true;
538 $results['match_string'] = $matches[0];
539 }
540 }
541 }
542
543 return $results;
544 }
545
546
547 /**
548 * リクエスト�
549 報�
550 �列のうちkeyの値を使用するルール判定
551 *
552 * @param array $waf_rule
553 * @param string $request_items_key
554 * @param string $variable
555 * @return array
556 */
557 public function check_request_item_key( $waf_rule, $request_items_key, $variable ): array {
558 $results = array(
559 'is_matched' => false,
560 'match_string' => '',
561 );
562 $matches = array();
563
564 // リクエスト�
565 報�
566 �列のkeyの変換
567 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
568
569 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_key, $matches ) ) {
570 if ( ! empty( $matches ) ) {
571 if ( false === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
572 // 除外の設定が無ければマッチしたと判定
573 $results['is_matched'] = true;
574 $results['match_string'] = $matches[0];
575 }
576 }
577 }
578
579 return $results;
580 }
581
582
583 /**
584 * リクエスト�
585 報�
586 �列のうち文字列の値を使用するルール判定
587 *
588 * @param array $waf_rule
589 * @param array $request_items
590 * @param string $variable
591 * @param array $chain_items
592 * @return array
593 */
594 public function check_request_item_strings( $waf_rule, $request_items, $variable, $chain_items ): array {
595 $results = array(
596 'is_matched' => false,
597 'chain_items' => array(),
598 'skip' => 0,
599 'skipafter' => '',
600 'match_results' => array(),
601 );
602
603 if ( empty( $request_items[ $variable ] ) ) {
604 return $results;
605 }
606
607 if ( $variable === self::VARIABLE_XML ) {
608 if ( $request_items[ self::VARIABLE_XML ] === 'xml_parse_failed' ) {
609 return $results;
610 } else {
611 $request_items[ self::VARIABLE_XML ] = str_replace( 'xml_parse_failed', '', $request_items[ self::VARIABLE_XML ] );
612 }
613 }
614
615 // リクエスト�
616 報の変換
617 $tmp_string = $this->transform( $waf_rule['transformations'], $request_items[ $variable ] );
618
619 // REQUEST_FILENAMEに関しては、urlデコード済の値も判定し、マッチしたら結果を出力
620 if ( $variable === self::VARIABLE_REQUEST_FILENAME ) {
621 // リクエスト�
622 報のデコード&変換
623 $request_items_urldecoded = urldecode( $request_items[ $variable ] );
624 $tmp_urldecoded = $this->transform( $waf_rule['transformations'], $request_items_urldecoded );
625
626 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_urldecoded, $matches ) ) {
627 if ( ! empty( $matches ) ) {
628 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
629 return $results;
630 }
631 }
632 }
633
634 if ( false === preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_string, $matches ) ) {
635 // preg_matchが失敗したときは終了
636 return $results;
637 }
638
639 if ( empty( $matches ) ) {
640 // 正規表現パターンにマッチしなければ終了
641 return $results;
642 }
643
644 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
645
646 return $results;
647 }
648
649
650 /**
651 * REST APIエンドポイントを取得(パーマリンク設定に依存しない)
652 *
653 * @param array $request_items
654 * @return string
655 */
656 public function get_rest_endpoint( $request_items ): string {
657 // パーマリンクが「基本」の場合はrest_routeパラメータにエンドポイントが含まれる
658 if ( isset( $request_items['args']['rest_route'] ) && is_string( $request_items['args']['rest_route'] ) ) {
659 return $request_items['args']['rest_route'];
660 }
661
662 // その他のパーマリンク設定の場合はrequest_filenameを使用
663 return $request_items['request_filename'];
664 }
665
666
667 /**
668 * WordPress管理画面での特定の処理に対し、特定のルールを除外する
669 *
670 * @param string $rule_id
671 * @param array $request_items
672 * @param array $remove_rules
673 * @return array
674 */
675 public function is_remove_rule( $rule_id, $request_items, $remove_rules, $acf_post_types, $cptui_post_types ): array {
676 $is_rule_removed = false;
677 $modify_remove_variables = array();
678 $modify_variables = array();
679
680 if ( isset( $remove_rules['woocommerce'] ) ) {
681 if ( in_array( $rule_id, $remove_rules['woocommerce'], true ) ) {
682 $sbjs_cookie_keys = preg_grep( '/^sbjs_.+/', array_keys( $request_items['request_cookies'] ) );
683
684 // sourcebusterの除外(woocommerce)
685 if ( ! empty( $sbjs_cookie_keys ) ) {
686 foreach ( $sbjs_cookie_keys as $key ) {
687 if ( preg_match( '/(\;|\||\`)\W*?\b(?:(?:c(?:h(?:grp|mod|own|sh)|md|pp)|p(?:asswd|ython|erl|ing|s)|n(?:asm|map|c)|f(?:inger|tp)|(?:kil|mai)l|(?:xte)?rm|ls(?:of)?|telnet|uname|echo|id)\b|g(?:\+\+|cc\b))/i', $request_items['request_cookies'][ $key ] ) === 1 ) {
688 $is_rule_removed = true;
689 break;
690 }
691 }
692 }
693 }
694 }
695
696 if ( isset( $remove_rules['ajax_customize'] ) ) {
697 // カスタマイズ操作、オートセーブ時の除外
698 if ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
699 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
700 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
701 $action = $request_items['args']['action'] ?? '';
702 if ( $action === 'update-widget' ) {
703 // actionがupdate-widgetの場合はルール�
704 �体を除外(cocoon)
705 $is_rule_removed = true;
706 } else {
707 // それ以外の場合はcustomized, customize_changeset_dataキーのみ除外
708 $modify_remove_variables['args'] = array( 'customized', 'customize_changeset_data' );
709 }
710 }
711 }
712 // ウィジェット保存時(cocoonテーマ)の除外
713 } elseif ( preg_match( '/wp-admin\/widgets\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
714 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
715 if ( ( $request_items['args']['action'] ?? '' ) === 'save-widget' ) {
716 $is_rule_removed = true;
717 }
718 }
719 // メニュー操作時(cocoonテーマ)の除外
720 } elseif ( preg_match( '/wp-admin\/nav-menus\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
721 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
722 $action = $request_items['args']['action'] ?? '';
723 if ( $action === 'update' || $action === 'edit' ) {
724 $is_rule_removed = true;
725 }
726 }
727 }
728 }
729
730 if ( isset( $remove_rules['rest_api'] ) ) {
731 $rest_endpoint = $this->get_rest_endpoint( $request_items );
732
733 // 投稿・編集(templates, blocks, template-parts, navigation, pages, posts)の場合はcontentキーを除外
734 if ( preg_match( '/templates|blocks|template-parts|navigation|pages|posts/', $rest_endpoint ) === 1 ) {
735 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
736 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
737 $modify_remove_variables['args'] = array( 'content' );
738 }
739 }
740
741 // global-styles の場合はstylesキーを除外
742 } elseif ( preg_match( '/global-styles/', $rest_endpoint ) === 1 ) {
743 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
744 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
745 $modify_remove_variables['args'] = array( '/^styles/' );
746 }
747 }
748
749 // batch の場合はrequestsキーを除外
750 } elseif ( preg_match( '/batch/', $rest_endpoint ) === 1 ) {
751 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
752 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
753 $modify_remove_variables['args'] = array( '/^requests/' );
754 }
755 }
756
757 // 投稿・編集の操作は特定のルールを除外する(post.php)
758 } elseif ( preg_match( '/wp-admin\/post\.php/', $request_items['request_filename'] ) === 1 ) {
759 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
760 $action = $request_items['args']['action'] ?? '';
761 if ( $action === 'editpost' ) {
762 $is_rule_removed = true;
763 } elseif ( $action === 'post-quickdraft-save' ) {
764 $modify_remove_variables['args'] = array( 'content' );
765 }
766 }
767
768 // カスタマイズ機能からの投稿作成時の除外
769 } elseif ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
770 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
771 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
772 $action = $request_items['args']['action'] ?? '';
773 if ( $action === 'customize-nav-menus-insert-auto-draft' ) {
774 $modify_remove_variables['args'] = array( '/^params/' );
775 }
776 }
777 }
778
779 // nishiki の場合はcontentキーを除外
780 } elseif ( preg_match( '/wp\/v2\/nishiki_pro_(patterns|content)/', $rest_endpoint ) === 1 ) {
781 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
782 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
783 $modify_remove_variables['args'] = array( 'content' );
784 }
785 }
786
787 // xerite の場合はcontentキーを除外
788 } elseif ( preg_match( '/wp\/v2\/xw_block_patterns/', $rest_endpoint ) === 1 ) {
789 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
790 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
791 $modify_remove_variables['args'] = array( 'content' );
792 }
793 }
794
795 // Lightning の場合はcontentキーを除外
796 } elseif ( preg_match( '/wp\/v2\/(cta|vk-block-patterns)/', $rest_endpoint ) === 1 ) {
797 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
798 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
799 $modify_remove_variables['args'] = array( 'content' );
800 }
801 }
802
803 // SWELL の場合はcontentキーを除外
804 } elseif ( preg_match( '/wp\/v2\/(lp|blog_parts)/', $rest_endpoint ) === 1 ) {
805 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
806 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
807 $modify_remove_variables['args'] = array( 'content' );
808 }
809 }
810
811 // Snow Monkey の場合はcontentキーを除外
812 } elseif ( preg_match( '/wp\/v2\/snow-monkey-search/', $rest_endpoint ) === 1 ) {
813 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
814 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
815 $modify_remove_variables['args'] = array( 'content' );
816 }
817 }
818
819 // Advanced Custom Fields の場合はcontentキーを除外
820 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
821 } elseif ( ! empty( $acf_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
822 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
823 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
824 $modify_remove_variables['args'] = array( 'content' );
825 }
826 }
827
828 // Custom Post Type UI の場合はcontentキーを除外
829 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
830 } elseif ( ! empty( $cptui_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $cptui_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
831 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
832 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
833 $modify_remove_variables['args'] = array( 'content' );
834 }
835 }
836 }
837 }
838
839 // cocoonテーマでの除外処理(theme-func-text, theme-settings, theme-ranking, theme-affiliate-tag)
840 if ( isset( $remove_rules['cocoon'] ) ) {
841 $referer = $_SERVER['HTTP_REFERER'] ?? '';
842 if ( preg_match( '/wp-admin\/admin\.php\?page\=theme-(func-text|settings|ranking|affiliate-tag)/', $referer ) === 1 ) {
843 if ( in_array( $rule_id, $remove_rules['cocoon'], true ) ) {
844 $action = $request_items['args']['action'] ?? '';
845 if ( $action === 'new' || $action === 'edit' ) {
846 $is_rule_removed = true;
847 }
848
849 if ( isset( $request_items['args']['comment_information_message'] ) ) {
850 $is_rule_removed = true;
851 }
852 }
853 }
854 }
855
856 if ( isset( $remove_rules['emanon'] ) ) {
857 // emanonルール除外
858 if ( preg_match( '/wp-admin\/admin.php\?page\=emanon_setting_page/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
859 if ( in_array( $rule_id, $remove_rules['emanon'], true ) ) {
860 if ( ( $request_items['args']['action'] ?? '' ) === 'delete_transients_emanon_setting' ) {
861 $is_rule_removed = true;
862 }
863 }
864 }
865 }
866
867 if ( isset( $remove_rules['vkexunit'] ) ) {
868 // vkExUnitルール除外(メイン設定)
869 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_main_setting/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
870 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
871 if ( preg_match( '/page\=vkExUnit_main_setting/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
872 $is_rule_removed = true;
873 }
874 }
875 }
876
877 // vkExUnitルール除外(cssカスタマイズ)
878 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_css_customize/', $_SERVER['REQUEST_URI'] ?? '' ) === 1 ) {
879 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
880 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
881 if ( strpos( $wp_http_referer, '/wp-admin/admin.php?page=vkExUnit_css_customize' ) !== false ) {
882 $is_rule_removed = true;
883 }
884 }
885 }
886 }
887
888 if ( isset( $remove_rules['nishiki'] ) ) {
889 // nishikiルール除外
890 if ( preg_match( '/wp-admin\/admin.php\?page\=nishiki-pro-general\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
891 if ( in_array( $rule_id, $remove_rules['nishiki'], true ) ) {
892 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
893 $is_rule_removed = true;
894 }
895 }
896 }
897 }
898
899 if ( isset( $remove_rules['swell'] ) ) {
900 // swellルール除外
901 if ( preg_match( '/wp-admin\/admin.php\?page\=swell_settings_editor/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
902 if ( in_array( $rule_id, $remove_rules['swell'], true ) ) {
903 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
904 $is_rule_removed = true;
905 }
906 }
907 }
908 }
909
910 if ( isset( $remove_rules['comment'] ) ) {
911 // コメント編集時の除外
912 if ( preg_match( '/wp-admin\/comment\.php/', $request_items['request_filename'] ?? '' ) === 1 ) {
913 if ( in_array( $rule_id, $remove_rules['comment'], true ) ) {
914 if ( ( $request_items['args']['action'] ?? '' ) === 'editedcomment' ) {
915 $is_rule_removed = true;
916 }
917 }
918 }
919 }
920
921 if ( isset( $remove_rules['ajax_editor'] ) ) {
922 // テーマ・プラグインエディターの操作時の除外
923 if ( preg_match( '/wp-admin\/admin-ajax\.php/', $request_items['request_filename'] ) === 1 ) {
924 if ( in_array( $rule_id, $remove_rules['ajax_editor'], true ) ) {
925 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
926 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $wp_http_referer ) === 1 ) {
927 $is_rule_removed = true;
928 }
929
930 // オートセーブ時
931 $screen_id = $request_items['args']['screen_id'] ?? '';
932 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $screen_id ) === 1 ) {
933 $is_rule_removed = true;
934 }
935 }
936 }
937 }
938
939 if ( isset( $remove_rules['rename_login_page'] ) ) {
940 $login_page_name = $remove_rules['rename_login_page']['login_page_name'];
941 $is_target_rule = in_array( $rule_id, $remove_rules['rename_login_page']['rule_ids'], true );
942
943 if ( $is_target_rule ) {
944 // ログインURL変更設定保存時:rename_login_page_nameキーの値のみ除外
945 if ( preg_match( '/wp-admin\/admin\.php/', $request_items['request_filename'] ) === 1 ) {
946 if ( ( $request_items['args']['page'] ?? '' ) === 'cloudsecurewp_rename_login_page' ) {
947 $modify_remove_variables['args'] = array( 'rename_login_page_name' );
948 }
949 }
950
951 // 変更後ログインURLへのアクセス時、またはリファラーが変更後ログインURLである時の対応
952 if ( ! empty( $login_page_name ) ) {
953 $login_page_pattern = '/\/' . preg_quote( $login_page_name, '/' ) . '(?:[\/\?#]|$)/';
954 $is_login_page_access = preg_match( $login_page_pattern, $request_items['request_filename'] ) === 1;
955 $is_login_page_referer = preg_match( $login_page_pattern, $_SERVER['HTTP_REFERER'] ?? '' ) === 1;
956
957 if ( $is_login_page_access ) {
958 // 1. URI自体に誤検知ワードが含まれるため、request_filenameを除外
959 $modify_variables[] = self::VARIABLE_REQUEST_FILENAME;
960
961 // 2. args の特定キー(リダイレクト系など)を除外
962 if ( ! isset( $modify_remove_variables['args'] ) ) {
963 $modify_remove_variables['args'] = array();
964 }
965 $modify_remove_variables['args'] = array_merge( $modify_remove_variables['args'], array( 'redirect_to', '_wp_http_referer' ) );
966 }
967
968 if ( $is_login_page_access || $is_login_page_referer ) {
969 // 3. リファラーヘッダーを除外
970 if ( ! isset( $modify_remove_variables['request_headers'] ) ) {
971 $modify_remove_variables['request_headers'] = array();
972 }
973 $modify_remove_variables['request_headers'][] = 'Referer';
974 }
975 }
976 }
977 }
978
979 return array(
980 'is_removed' => $is_rule_removed,
981 'modify_remove_variables' => $modify_remove_variables,
982 'modify_variables' => $modify_variables,
983 );
984 }
985
986 /**
987 * Advanced Custom Fieldsプラグイン除外対応
988 * 有効なカスタム投稿タイプキーを取得する
989 *
990 * @return array
991 */
992 public function get_acf_post_types(): array {
993 global $wpdb;
994 $active_plugins = get_option( 'active_plugins' );
995 $acf_post_types = array();
996
997 if ( is_array( $active_plugins ) && preg_match( '/advanced-custom-fields/', implode( ',', $active_plugins ) ) ) {
998 $results = $wpdb->get_results(
999 $wpdb->prepare(
1000 "SELECT post_content
1001 FROM {$wpdb->posts}
1002 WHERE post_type = %s
1003 AND post_status = %s",
1004 'acf-post-type',
1005 'publish'
1006 )
1007 );
1008
1009 if ( ! empty( $results ) ) {
1010 foreach ( $results as $result ) {
1011 $post_content = unserialize( $result->post_content, [ 'allowed_classes' => false ] );
1012
1013 if ( is_array( $post_content ) && isset( $post_content['post_type'] ) ) {
1014 $acf_post_types[] = $post_content['post_type'];
1015 }
1016 }
1017 }
1018 }
1019
1020 return $acf_post_types;
1021 }
1022
1023
1024 /**
1025 * Custom Post Type UIプラグイン除外対応
1026 * 有効なカスタム投稿タイプキーを取得する
1027 *
1028 * @return array
1029 */
1030 public function get_cptui_post_types(): array {
1031 $active_plugins = get_option( 'active_plugins' );
1032 $cptui_post_types = array();
1033
1034 if ( is_array( $active_plugins ) && preg_match( '/custom-post-type-ui/', implode( ',', $active_plugins ) ) ) {
1035 $cptui_data = get_option( 'cptui_post_types' );
1036
1037 if ( is_string( $cptui_data ) ) {
1038 $cptui_data = unserialize( $cptui_data, [ 'allowed_classes' => false ] );
1039 }
1040
1041 if ( is_array( $cptui_data ) ) {
1042 foreach ( $cptui_data as $post_type ) {
1043 if ( is_array( $post_type ) && isset( $post_type['name'] ) ) {
1044 $cptui_post_types[] = $post_type['name'];
1045 }
1046 }
1047 }
1048 }
1049
1050 return $cptui_post_types;
1051 }
1052
1053
1054 /**
1055 * waf_engine
1056 *
1057 * @param array $waf_rules
1058 * @param array $locationmatch_rules
1059 * @param int $available_rules
1060 * @param array $remove_rules
1061 * @return array
1062 */
1063 public function waf_engine( $waf_rules, $locationmatch_rules, $available_rules, $remove_rules ): array {
1064 $request_items = $this->get_request_items();
1065
1066 $locationmatch_removed_rule_ids = $this->locationmatch_remove_rules( $locationmatch_rules, $_SERVER['REQUEST_URI'] ?? '' );
1067 $skip = 0;
1068 $skipafter = '';
1069 $chain_items = array();
1070 $tmp_match_results = array();
1071
1072 // Advanced Custom Fieldsプラグイン除外対応で追加
1073 $acf_post_types = $this->get_acf_post_types();
1074 // Custom Post Type UIプラグイン除外対応で追加
1075 $cptui_post_types = $this->get_cptui_post_types();
1076
1077 foreach ( $waf_rules as $waf_rule ) {
1078 // 前回マッチしたルールからskipの設定を引き継いでいる場合はスキップ
1079 if ( $this->is_skip_enabled( $skip ) ) {
1080 $skip--;
1081 continue;
1082 }
1083
1084 // 前回マッチしたルールからskipafterの設定を引き継いでいる場合は現在のルールIDと比較し、一致するまでスキップ
1085 // ルールIDの比較結果が同じの場合は、$skipafterを初期化して次のルールから判定を行う
1086 if ( $this->is_skipafter_enabled( $skipafter ) ) {
1087 if ( $skipafter !== $waf_rule['id'] ) {
1088 continue;
1089 } else {
1090 $skipafter = '';
1091 continue;
1092 }
1093 }
1094
1095 // LocationMatchによるルールの除外があるか確認。ある場合は現在のルールIDと比較し、一致する場合はスキップ
1096 if ( ! empty( $locationmatch_removed_rule_ids ) ) {
1097 if ( in_array( $waf_rule['id'], $locationmatch_removed_rule_ids, true ) ) {
1098 continue;
1099 }
1100 }
1101
1102 // ルールにvariables設定がない場合、skip,akipafterの設定を確認して次のルール判定へ
1103 if ( empty( $waf_rule['variables'] ) ) {
1104 $skip = $this->check_skip( $waf_rule['skip'] );
1105 $skipafter = $this->check_skipafter( $waf_rule['skipafter'] );
1106 continue;
1107 }
1108
1109 // 特定の操作の場合、特定のルールを除外する
1110 $remove_rule_result = $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types, $cptui_post_types );
1111
1112 if ( $remove_rule_result['is_removed'] ) {
1113 continue;
1114 }
1115
1116 // ルールのremove_variablesを動的に変更
1117 if ( ! empty( $remove_rule_result['modify_remove_variables'] ) ) {
1118 $waf_rule['remove_variables'] = array_merge_recursive(
1119 $waf_rule['remove_variables'],
1120 $remove_rule_result['modify_remove_variables']
1121 );
1122 }
1123
1124 // ルールのvariablesを動的に変更
1125 if ( ! empty( $remove_rule_result['modify_variables'] ) ) {
1126 $waf_rule['variables'] = array_diff(
1127 $waf_rule['variables'],
1128 $remove_rule_result['modify_variables']
1129 );
1130 }
1131
1132 foreach ( $waf_rule['variables'] as $variable ) {
1133 switch ( $variable ) {
1134 case self::VARIABLE_ARGS:
1135 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS, $chain_items );
1136 break;
1137
1138 case self::VARIABLE_ARGS_NAMES:
1139 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_NAMES, $chain_items );
1140 break;
1141
1142 case self::VARIABLE_REQUEST_COOKIES:
1143 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES, $chain_items );
1144 break;
1145
1146 case self::VARIABLE_REQUEST_COOKIES_NAMES:
1147 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES_NAMES, $chain_items );
1148 break;
1149
1150 case self::VARIABLE_REQUEST_HEADERS:
1151 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_HEADERS, $chain_items );
1152 break;
1153
1154 case self::VARIABLE_REQUEST_FILENAME:
1155 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_REQUEST_FILENAME, $chain_items );
1156 break;
1157
1158 case self::VARIABLE_XML:
1159 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_XML, $chain_items );
1160 break;
1161 }
1162
1163 $is_matched = $results['is_matched'];
1164
1165 if ( $is_matched ) {
1166 $skip = $results['skip'];
1167 $skipafter = $results['skipafter'];
1168 $chain_items = $results['chain_items'];
1169 $match_results = $results['match_results'];
1170
1171 // マッチしたが、chain,skip,skipafterの設定がある場合は次のルール判定へ
1172 if ( ! empty( $chain_items ) || 0 < $skip || ! empty( $skipafter ) ) {
1173 break;
1174 }
1175
1176 // マッチしたが、除外設定されているルールの場合は値を保持して次のルール判定へ
1177 if ( ( $waf_rule['attack'] & $available_rules ) === 0 ) {
1178 $tmp_match_results = $match_results;
1179 break;
1180 }
1181
1182 // マッチした結果がある場合は終了処理へ(ログ記述、通知、画面表示)
1183 if ( ! empty( $match_results ) ) {
1184 $match_results['is_deny'] = true;
1185 $match_results['is_write_log'] = true;
1186
1187 return $match_results;
1188 }
1189 }
1190
1191 $chain_items = array();
1192 }
1193 }
1194
1195 if ( ! empty( $tmp_match_results ) ) {
1196 $match_results = $tmp_match_results;
1197 $match_results['is_deny'] = false;
1198 $match_results['is_write_log'] = true;
1199
1200 } else {
1201 $match_results['is_deny'] = false;
1202 $match_results['is_write_log'] = false;
1203 }
1204
1205 return $match_results;
1206 }
1207 }
1208