PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.6
CloudSecure WP Security v1.4.6
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 3 months ago cli 8 months ago lib 3 months ago captcha.php 3 months ago cloudsecure-wp.php 3 months ago common.php 3 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 3 months ago disable-restapi.php 7 months ago disable-xmlrpc.php 1 year ago htaccess.php 3 months ago login-log.php 3 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 3 months ago two-factor-authentication.php 3 months ago unify-messages.php 2 years ago update-notice.php 7 months ago waf-engine.php 3 months ago waf.php 3 months ago
waf-engine.php
1157 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
679 if ( isset( $remove_rules['woocommerce'] ) ) {
680 if ( in_array( $rule_id, $remove_rules['woocommerce'], true ) ) {
681 $sbjs_cookie_keys = preg_grep( '/^sbjs_.+/', array_keys( $request_items['request_cookies'] ) );
682
683 // sourcebusterの除外(woocommerce)
684 if ( ! empty( $sbjs_cookie_keys ) ) {
685 foreach ( $sbjs_cookie_keys as $key ) {
686 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 ) {
687 $is_rule_removed = true;
688 break;
689 }
690 }
691 }
692 }
693 }
694
695 if ( isset( $remove_rules['ajax_customize'] ) ) {
696 // カスタマイズ操作、オートセーブ時の除外
697 if ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
698 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
699 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
700 $action = $request_items['args']['action'] ?? '';
701 if ( $action === 'update-widget' ) {
702 // actionがupdate-widgetの場合はルール�
703 �体を除外(cocoon)
704 $is_rule_removed = true;
705 } else {
706 // それ以外の場合はcustomized, customize_changeset_dataキーのみ除外
707 $modify_remove_variables['args'] = array( 'customized', 'customize_changeset_data' );
708 }
709 }
710 }
711 // ウィジェット保存時(cocoonテーマ)の除外
712 } elseif ( preg_match( '/wp-admin\/widgets\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
713 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
714 if ( ( $request_items['args']['action'] ?? '' ) === 'save-widget' ) {
715 $is_rule_removed = true;
716 }
717 }
718 // メニュー操作時(cocoonテーマ)の除外
719 } elseif ( preg_match( '/wp-admin\/nav-menus\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
720 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
721 $action = $request_items['args']['action'] ?? '';
722 if ( $action === 'update' || $action === 'edit' ) {
723 $is_rule_removed = true;
724 }
725 }
726 }
727 }
728
729 if ( isset( $remove_rules['rest_api'] ) ) {
730 $rest_endpoint = $this->get_rest_endpoint( $request_items );
731
732 // 投稿・編集(templates, blocks, template-parts, navigation, pages, posts)の場合はcontentキーを除外
733 if ( preg_match( '/templates|blocks|template-parts|navigation|pages|posts/', $rest_endpoint ) === 1 ) {
734 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
735 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
736 $modify_remove_variables['args'] = array( 'content' );
737 }
738 }
739
740 // global-styles の場合はstylesキーを除外
741 } elseif ( preg_match( '/global-styles/', $rest_endpoint ) === 1 ) {
742 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
743 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
744 $modify_remove_variables['args'] = array( '/^styles/' );
745 }
746 }
747
748 // batch の場合はrequestsキーを除外
749 } elseif ( preg_match( '/batch/', $rest_endpoint ) === 1 ) {
750 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
751 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
752 $modify_remove_variables['args'] = array( '/^requests/' );
753 }
754 }
755
756 // 投稿・編集の操作は特定のルールを除外する(post.php)
757 } elseif ( preg_match( '/wp-admin\/post\.php/', $request_items['request_filename'] ) === 1 ) {
758 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
759 $action = $request_items['args']['action'] ?? '';
760 if ( $action === 'editpost' ) {
761 $is_rule_removed = true;
762 } elseif ( $action === 'post-quickdraft-save' ) {
763 $modify_remove_variables['args'] = array( 'content' );
764 }
765 }
766
767 // カスタマイズ機能からの投稿作成時の除外
768 } elseif ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
769 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
770 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
771 $action = $request_items['args']['action'] ?? '';
772 if ( $action === 'customize-nav-menus-insert-auto-draft' ) {
773 $modify_remove_variables['args'] = array( '/^params/' );
774 }
775 }
776 }
777
778 // nishiki の場合はcontentキーを除外
779 } elseif ( preg_match( '/wp\/v2\/nishiki_pro_(patterns|content)/', $rest_endpoint ) === 1 ) {
780 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
781 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
782 $modify_remove_variables['args'] = array( 'content' );
783 }
784 }
785
786 // xerite の場合はcontentキーを除外
787 } elseif ( preg_match( '/wp\/v2\/xw_block_patterns/', $rest_endpoint ) === 1 ) {
788 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
789 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
790 $modify_remove_variables['args'] = array( 'content' );
791 }
792 }
793
794 // Lightning の場合はcontentキーを除外
795 } elseif ( preg_match( '/wp\/v2\/(cta|vk-block-patterns)/', $rest_endpoint ) === 1 ) {
796 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
797 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
798 $modify_remove_variables['args'] = array( 'content' );
799 }
800 }
801
802 // SWELL の場合はcontentキーを除外
803 } elseif ( preg_match( '/wp\/v2\/(lp|blog_parts)/', $rest_endpoint ) === 1 ) {
804 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
805 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
806 $modify_remove_variables['args'] = array( 'content' );
807 }
808 }
809
810 // Snow Monkey の場合はcontentキーを除外
811 } elseif ( preg_match( '/wp\/v2\/snow-monkey-search/', $rest_endpoint ) === 1 ) {
812 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
813 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
814 $modify_remove_variables['args'] = array( 'content' );
815 }
816 }
817
818 // Advanced Custom Fields の場合はcontentキーを除外
819 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
820 } elseif ( ! empty( $acf_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
821 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
822 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
823 $modify_remove_variables['args'] = array( 'content' );
824 }
825 }
826
827 // Custom Post Type UI の場合はcontentキーを除外
828 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
829 } elseif ( ! empty( $cptui_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $cptui_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
830 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
831 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
832 $modify_remove_variables['args'] = array( 'content' );
833 }
834 }
835 }
836 }
837
838 // cocoonテーマでの除外処理(theme-func-text, theme-settings, theme-ranking, theme-affiliate-tag)
839 if ( isset( $remove_rules['cocoon'] ) ) {
840 $referer = $_SERVER['HTTP_REFERER'] ?? '';
841 if ( preg_match( '/wp-admin\/admin\.php\?page\=theme-(func-text|settings|ranking|affiliate-tag)/', $referer ) === 1 ) {
842 if ( in_array( $rule_id, $remove_rules['cocoon'], true ) ) {
843 $action = $request_items['args']['action'] ?? '';
844 if ( $action === 'new' || $action === 'edit' ) {
845 $is_rule_removed = true;
846 }
847
848 if ( isset( $request_items['args']['comment_information_message'] ) ) {
849 $is_rule_removed = true;
850 }
851 }
852 }
853 }
854
855 if ( isset( $remove_rules['emanon'] ) ) {
856 // emanonルール除外
857 if ( preg_match( '/wp-admin\/admin.php\?page\=emanon_setting_page/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
858 if ( in_array( $rule_id, $remove_rules['emanon'], true ) ) {
859 if ( ( $request_items['args']['action'] ?? '' ) === 'delete_transients_emanon_setting' ) {
860 $is_rule_removed = true;
861 }
862 }
863 }
864 }
865
866 if ( isset( $remove_rules['vkexunit'] ) ) {
867 // vkExUnitルール除外(メイン設定)
868 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_main_setting/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
869 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
870 if ( preg_match( '/page\=vkExUnit_main_setting/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
871 $is_rule_removed = true;
872 }
873 }
874 }
875
876 // vkExUnitルール除外(cssカスタマイズ)
877 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_css_customize/', $_SERVER['REQUEST_URI'] ?? '' ) === 1 ) {
878 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
879 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
880 if ( strpos( $wp_http_referer, '/wp-admin/admin.php?page=vkExUnit_css_customize' ) !== false ) {
881 $is_rule_removed = true;
882 }
883 }
884 }
885 }
886
887 if ( isset( $remove_rules['nishiki'] ) ) {
888 // nishikiルール除外
889 if ( preg_match( '/wp-admin\/admin.php\?page\=nishiki-pro-general\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
890 if ( in_array( $rule_id, $remove_rules['nishiki'], true ) ) {
891 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
892 $is_rule_removed = true;
893 }
894 }
895 }
896 }
897
898 if ( isset( $remove_rules['swell'] ) ) {
899 // swellルール除外
900 if ( preg_match( '/wp-admin\/admin.php\?page\=swell_settings_editor/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
901 if ( in_array( $rule_id, $remove_rules['swell'], true ) ) {
902 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
903 $is_rule_removed = true;
904 }
905 }
906 }
907 }
908
909 if ( isset( $remove_rules['comment'] ) ) {
910 // コメント編集時の除外
911 if ( preg_match( '/wp-admin\/comment\.php/', $request_items['request_filename'] ?? '' ) === 1 ) {
912 if ( in_array( $rule_id, $remove_rules['comment'], true ) ) {
913 if ( ( $request_items['args']['action'] ?? '' ) === 'editedcomment' ) {
914 $is_rule_removed = true;
915 }
916 }
917 }
918 }
919
920 if ( isset( $remove_rules['ajax_editor'] ) ) {
921 // テーマ・プラグインエディターの操作時の除外
922 if ( preg_match( '/wp-admin\/admin-ajax\.php/', $request_items['request_filename'] ) === 1 ) {
923 if ( in_array( $rule_id, $remove_rules['ajax_editor'], true ) ) {
924 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
925 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $wp_http_referer ) === 1 ) {
926 $is_rule_removed = true;
927 }
928
929 // オートセーブ時
930 $screen_id = $request_items['args']['screen_id'] ?? '';
931 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $screen_id ) === 1 ) {
932 $is_rule_removed = true;
933 }
934 }
935 }
936 }
937 return array(
938 'is_removed' => $is_rule_removed,
939 'modify_remove_variables' => $modify_remove_variables,
940 );
941 }
942
943 /**
944 * Advanced Custom Fieldsプラグイン除外対応
945 * 有効なカスタム投稿タイプキーを取得する
946 *
947 * @return array
948 */
949 public function get_acf_post_types(): array {
950 global $wpdb;
951 $active_plugins = get_option( 'active_plugins' );
952 $acf_post_types = array();
953
954 if ( is_array( $active_plugins ) && preg_match( '/advanced-custom-fields/', implode( ',', $active_plugins ) ) ) {
955 $results = $wpdb->get_results(
956 $wpdb->prepare(
957 "SELECT post_content
958 FROM {$wpdb->posts}
959 WHERE post_type = %s
960 AND post_status = %s",
961 'acf-post-type',
962 'publish'
963 )
964 );
965
966 if ( ! empty( $results ) ) {
967 foreach ( $results as $result ) {
968 $post_content = unserialize( $result->post_content, [ 'allowed_classes' => false ] );
969
970 if ( is_array( $post_content ) && isset( $post_content['post_type'] ) ) {
971 $acf_post_types[] = $post_content['post_type'];
972 }
973 }
974 }
975 }
976
977 return $acf_post_types;
978 }
979
980
981 /**
982 * Custom Post Type UIプラグイン除外対応
983 * 有効なカスタム投稿タイプキーを取得する
984 *
985 * @return array
986 */
987 public function get_cptui_post_types(): array {
988 $active_plugins = get_option( 'active_plugins' );
989 $cptui_post_types = array();
990
991 if ( is_array( $active_plugins ) && preg_match( '/custom-post-type-ui/', implode( ',', $active_plugins ) ) ) {
992 $cptui_data = get_option( 'cptui_post_types' );
993
994 if ( is_string( $cptui_data ) ) {
995 $cptui_data = unserialize( $cptui_data, [ 'allowed_classes' => false ] );
996 }
997
998 if ( is_array( $cptui_data ) ) {
999 foreach ( $cptui_data as $post_type ) {
1000 if ( is_array( $post_type ) && isset( $post_type['name'] ) ) {
1001 $cptui_post_types[] = $post_type['name'];
1002 }
1003 }
1004 }
1005 }
1006
1007 return $cptui_post_types;
1008 }
1009
1010
1011 /**
1012 * waf_engine
1013 *
1014 * @param array $waf_rules
1015 * @param array $locationmatch_rules
1016 * @param int $available_rules
1017 * @param array $remove_rules
1018 * @return array
1019 */
1020 public function waf_engine( $waf_rules, $locationmatch_rules, $available_rules, $remove_rules ): array {
1021 $request_items = $this->get_request_items();
1022
1023 $locationmatch_removed_rule_ids = $this->locationmatch_remove_rules( $locationmatch_rules, $_SERVER['REQUEST_URI'] ?? '' );
1024 $skip = 0;
1025 $skipafter = '';
1026 $chain_items = array();
1027 $tmp_match_results = array();
1028
1029 // Advanced Custom Fieldsプラグイン除外対応で追加
1030 $acf_post_types = $this->get_acf_post_types();
1031 // Custom Post Type UIプラグイン除外対応で追加
1032 $cptui_post_types = $this->get_cptui_post_types();
1033
1034 foreach ( $waf_rules as $waf_rule ) {
1035 // 前回マッチしたルールからskipの設定を引き継いでいる場合はスキップ
1036 if ( $this->is_skip_enabled( $skip ) ) {
1037 $skip--;
1038 continue;
1039 }
1040
1041 // 前回マッチしたルールからskipafterの設定を引き継いでいる場合は現在のルールIDと比較し、一致するまでスキップ
1042 // ルールIDの比較結果が同じの場合は、$skipafterを初期化して次のルールから判定を行う
1043 if ( $this->is_skipafter_enabled( $skipafter ) ) {
1044 if ( $skipafter !== $waf_rule['id'] ) {
1045 continue;
1046 } else {
1047 $skipafter = '';
1048 continue;
1049 }
1050 }
1051
1052 // LocationMatchによるルールの除外があるか確認。ある場合は現在のルールIDと比較し、一致する場合はスキップ
1053 if ( ! empty( $locationmatch_removed_rule_ids ) ) {
1054 if ( in_array( $waf_rule['id'], $locationmatch_removed_rule_ids, true ) ) {
1055 continue;
1056 }
1057 }
1058
1059 // ルールにvariables設定がない場合、skip,akipafterの設定を確認して次のルール判定へ
1060 if ( empty( $waf_rule['variables'] ) ) {
1061 $skip = $this->check_skip( $waf_rule['skip'] );
1062 $skipafter = $this->check_skipafter( $waf_rule['skipafter'] );
1063 continue;
1064 }
1065
1066 // 特定の操作の場合、特定のルールを除外する
1067 $remove_rule_result = $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types, $cptui_post_types );
1068
1069 if ( $remove_rule_result['is_removed'] ) {
1070 continue;
1071 }
1072
1073 // ルールのremove_variablesを動的に変更
1074 if ( ! empty( $remove_rule_result['modify_remove_variables'] ) ) {
1075 $waf_rule['remove_variables'] = array_merge_recursive(
1076 $waf_rule['remove_variables'],
1077 $remove_rule_result['modify_remove_variables']
1078 );
1079 }
1080
1081 foreach ( $waf_rule['variables'] as $variable ) {
1082 switch ( $variable ) {
1083 case self::VARIABLE_ARGS:
1084 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS, $chain_items );
1085 break;
1086
1087 case self::VARIABLE_ARGS_NAMES:
1088 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_NAMES, $chain_items );
1089 break;
1090
1091 case self::VARIABLE_REQUEST_COOKIES:
1092 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES, $chain_items );
1093 break;
1094
1095 case self::VARIABLE_REQUEST_COOKIES_NAMES:
1096 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES_NAMES, $chain_items );
1097 break;
1098
1099 case self::VARIABLE_REQUEST_HEADERS:
1100 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_HEADERS, $chain_items );
1101 break;
1102
1103 case self::VARIABLE_REQUEST_FILENAME:
1104 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_REQUEST_FILENAME, $chain_items );
1105 break;
1106
1107 case self::VARIABLE_XML:
1108 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_XML, $chain_items );
1109 break;
1110 }
1111
1112 $is_matched = $results['is_matched'];
1113
1114 if ( $is_matched ) {
1115 $skip = $results['skip'];
1116 $skipafter = $results['skipafter'];
1117 $chain_items = $results['chain_items'];
1118 $match_results = $results['match_results'];
1119
1120 // マッチしたが、chain,skip,skipafterの設定がある場合は次のルール判定へ
1121 if ( ! empty( $chain_items ) || 0 < $skip || ! empty( $skipafter ) ) {
1122 break;
1123 }
1124
1125 // マッチしたが、除外設定されているルールの場合は値を保持して次のルール判定へ
1126 if ( ( $waf_rule['attack'] & $available_rules ) === 0 ) {
1127 $tmp_match_results = $match_results;
1128 break;
1129 }
1130
1131 // マッチした結果がある場合は終了処理へ(ログ記述、通知、画面表示)
1132 if ( ! empty( $match_results ) ) {
1133 $match_results['is_deny'] = true;
1134 $match_results['is_write_log'] = true;
1135
1136 return $match_results;
1137 }
1138 }
1139
1140 $chain_items = array();
1141 }
1142 }
1143
1144 if ( ! empty( $tmp_match_results ) ) {
1145 $match_results = $tmp_match_results;
1146 $match_results['is_deny'] = false;
1147 $match_results['is_write_log'] = true;
1148
1149 } else {
1150 $match_results['is_deny'] = false;
1151 $match_results['is_write_log'] = false;
1152 }
1153
1154 return $match_results;
1155 }
1156 }
1157