PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.5
CloudSecure WP Security v1.4.5
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 4 months ago cli 8 months ago lib 4 months ago captcha.php 3 months ago cloudsecure-wp.php 3 months ago common.php 4 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 1 year ago disable-restapi.php 7 months ago disable-xmlrpc.php 1 year ago htaccess.php 5 months ago login-log.php 4 months ago login-notification.php 1 year ago rename-login-page.php 1 year ago restrict-admin-page.php 10 months ago server-error-notification.php 1 year ago two-factor-authentication.php 4 months ago unify-messages.php 2 years ago update-notice.php 7 months ago waf-engine.php 4 months ago waf.php 4 months ago
waf-engine.php
1155 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://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
320 } else {
321 $complete_url = '';
322 }
323
324 $match_results = array(
325 'id' => $rule['id'],
326 'attack' => $rule['attack'],
327 'variable' => $variable,
328 'matched' => $match_string,
329 'ip' => $request_items['ip'],
330 'access_at' => $request_items['access_at'],
331 'url' => $complete_url,
332 );
333
334 return $match_results;
335 }
336
337
338 /**
339 * chain_itemsの取得
340 *
341 * @param array $rule
342 * @return array
343 */
344 public function get_chain_items( $rule ): array {
345 $chain_items = array(
346 'id' => $rule['id'],
347 'attack' => $rule['attack'],
348 'skip' => $rule['skip'],
349 'skipafter' => $rule['skipafter'],
350 );
351
352 return $chain_items;
353 }
354
355
356 /**
357 * マッチしたルールの設定、結果を取得
358 *
359 * @param array $waf_rule
360 * @param array $request_items
361 * @param string $variable
362 * @param array $chain_items
363 * @param array $match_string
364 * @return array
365 */
366 public function get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $match_string ): array {
367 $results = array(
368 'is_matched' => true,
369 'chain_items' => array(),
370 'skip' => 0,
371 'skipafter' => '',
372 'match_results' => array(),
373 );
374
375 // 前ルールからchainの設定を引き継いでいるか確認
376 if ( ! empty( $chain_items ) ) {
377 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
378 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
379 $results['is_matched'] = false;
380
381 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $chain_items['skip'], $chain_items['skipafter'] ) ) {
382 $results['match_results'] = $this->get_match_results( $chain_items, $request_items, $variable, $match_string );
383
384 } elseif ( $waf_rule['chain'] ) {
385 // 現在のルールにchainの設定がある場合
386 $results['chain_items'] = $chain_items;
387
388 } else {
389 $results['skip'] = $this->check_skip( $chain_items['skip'] );
390 $results['skipafter'] = $this->check_skipafter( $chain_items['skipafter'] );
391 }
392 } else {
393 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
394 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
395 $results['is_matched'] = false;
396
397 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
398 $results['match_results'] = $this->get_match_results( $waf_rule, $request_items, $variable, $match_string );
399
400 } elseif ( $waf_rule['chain'] ) {
401 // 現在のルールにchainの設定がある場合
402 $results['chain_items'] = $this->get_chain_items( $waf_rule );
403
404 } else {
405 $results['skip'] = $this->check_skip( $waf_rule['skip'] );
406 $results['skipafter'] = $this->check_skipafter( $waf_rule['skipafter'] );
407 }
408 }
409 return $results;
410 }
411
412 /**
413 * ネストした�
414 �列をパース
415 *
416 * @param string $name
417 * @param array $array
418 */
419 public function parse_array( $name, $array ) {
420 $parsed_array = array();
421
422 foreach ( $array as $key => $val ) {
423 if ( is_array( $val ) ) {
424 $tmp_name = $name . '[' . $key . ']';
425 $tmp_parsed_array = $this->parse_array( $tmp_name, $val );
426 $parsed_array = array_merge( $parsed_array, $tmp_parsed_array );
427
428 } else {
429 $parsed_array_key = $name . '[' . $key . ']';
430 $parsed_array[ $parsed_array_key ] = $val ?? '';
431 }
432 }
433 return $parsed_array;
434 }
435
436
437 /**
438 * リクエスト�
439 報�
440 �列を使用するルール判定
441 *
442 * @param array $waf_rule
443 * @param array $request_items
444 * @param string $variable
445 * @param array $chain_items
446 * @return array
447 */
448 public function check_request_item_array( $waf_rule, $request_items, $variable, $chain_items ) {
449 $results = array(
450 'is_matched' => false,
451 'chain_items' => array(),
452 'skip' => 0,
453 'skipafter' => '',
454 'match_results' => array(),
455 );
456
457 $get_request_item_variable = preg_replace( '/_names/', '', $variable );
458
459 if ( ! isset( $get_request_item_variable ) ) {
460 return $results;
461 }
462
463 foreach ( $request_items[ $get_request_item_variable ] as $key => $val ) {
464
465 if ( ! isset( $val ) ) {
466 $val = '';
467 }
468
469 switch ( $variable ) {
470 case self::VARIABLE_ARGS:
471 case self::VARIABLE_REQUEST_COOKIES:
472 case self::VARIABLE_REQUEST_HEADERS:
473 if ( is_array( $val ) ) {
474 $parsed_vals = $this->parse_array( $key, $val );
475
476 foreach ( $parsed_vals as $key => $val ) {
477 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
478
479 if ( $checked_request_item['is_matched'] ) {
480 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
481 break;
482 }
483 }
484 } else {
485 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
486 }
487
488 break;
489
490 case self::VARIABLE_ARGS_NAMES:
491 case self::VARIABLE_REQUEST_COOKIES_NAMES:
492 $checked_request_item = $this->check_request_item_key( $waf_rule, $key, $variable );
493 break;
494 }
495
496 if ( $checked_request_item['is_matched'] ) {
497 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
498
499 break;
500 }
501 }
502 return $results;
503 }
504
505
506
507 /**
508 * リクエスト�
509 報�
510 �列のうちvalueの値を使用するルール判定
511 *
512 * @param array $waf_rule
513 * @param string $request_items_key
514 * @param string $request_items_value
515 * @param string $variable
516 * @return array
517 */
518 public function check_request_item_value( $waf_rule, $request_items_key, $request_items_value, $variable ): array {
519 $results = array(
520 'is_matched' => false,
521 'match_string' => '',
522 );
523 $matches = array();
524
525 // リクエスト�
526 報�
527 �列のkeyとvalueの変換
528 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
529 $tmp_val = $this->transform( $waf_rule['transformations'], $request_items_value );
530
531 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_val, $matches ) ) {
532 if ( ! empty( $matches ) ) {
533 if ( false === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
534 // 除外の設定が無ければマッチしたと判定
535 $results['is_matched'] = true;
536 $results['match_string'] = $matches[0];
537 }
538 }
539 }
540
541 return $results;
542 }
543
544
545 /**
546 * リクエスト�
547 報�
548 �列のうちkeyの値を使用するルール判定
549 *
550 * @param array $waf_rule
551 * @param string $request_items_key
552 * @param string $variable
553 * @return array
554 */
555 public function check_request_item_key( $waf_rule, $request_items_key, $variable ): array {
556 $results = array(
557 'is_matched' => false,
558 'match_string' => '',
559 );
560 $matches = array();
561
562 // リクエスト�
563 報�
564 �列のkeyの変換
565 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
566
567 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_key, $matches ) ) {
568 if ( ! empty( $matches ) ) {
569 if ( false === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
570 // 除外の設定が無ければマッチしたと判定
571 $results['is_matched'] = true;
572 $results['match_string'] = $matches[0];
573 }
574 }
575 }
576
577 return $results;
578 }
579
580
581 /**
582 * リクエスト�
583 報�
584 �列のうち文字列の値を使用するルール判定
585 *
586 * @param array $waf_rule
587 * @param array $request_items
588 * @param string $variable
589 * @param array $chain_items
590 * @return array
591 */
592 public function check_request_item_strings( $waf_rule, $request_items, $variable, $chain_items ): array {
593 $results = array(
594 'is_matched' => false,
595 'chain_items' => array(),
596 'skip' => 0,
597 'skipafter' => '',
598 'match_results' => array(),
599 );
600
601 if ( empty( $request_items[ $variable ] ) ) {
602 return $results;
603 }
604
605 if ( $variable === self::VARIABLE_XML ) {
606 if ( $request_items[ self::VARIABLE_XML ] === 'xml_parse_failed' ) {
607 return $results;
608 } else {
609 $request_items[ self::VARIABLE_XML ] = str_replace( 'xml_parse_failed', '', $request_items[ self::VARIABLE_XML ] );
610 }
611 }
612
613 // リクエスト�
614 報の変換
615 $tmp_string = $this->transform( $waf_rule['transformations'], $request_items[ $variable ] );
616
617 // REQUEST_FILENAMEに関しては、urlデコード済の値も判定し、マッチしたら結果を出力
618 if ( $variable === self::VARIABLE_REQUEST_FILENAME ) {
619 // リクエスト�
620 報のデコード&変換
621 $request_items_urldecoded = urldecode( $request_items[ $variable ] );
622 $tmp_urldecoded = $this->transform( $waf_rule['transformations'], $request_items_urldecoded );
623
624 if ( preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_urldecoded, $matches ) ) {
625 if ( ! empty( $matches ) ) {
626 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
627 return $results;
628 }
629 }
630 }
631
632 if ( false === preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_string, $matches ) ) {
633 // preg_matchが失敗したときは終了
634 return $results;
635 }
636
637 if ( empty( $matches ) ) {
638 // 正規表現パターンにマッチしなければ終了
639 return $results;
640 }
641
642 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
643
644 return $results;
645 }
646
647
648 /**
649 * REST APIエンドポイントを取得(パーマリンク設定に依存しない)
650 *
651 * @param array $request_items
652 * @return string
653 */
654 public function get_rest_endpoint( $request_items ): string {
655 // パーマリンクが「基本」の場合はrest_routeパラメータにエンドポイントが含まれる
656 if ( isset( $request_items['args']['rest_route'] ) && is_string( $request_items['args']['rest_route'] ) ) {
657 return $request_items['args']['rest_route'];
658 }
659
660 // その他のパーマリンク設定の場合はrequest_filenameを使用
661 return $request_items['request_filename'];
662 }
663
664
665 /**
666 * WordPress管理画面での特定の処理に対し、特定のルールを除外する
667 *
668 * @param string $rule_id
669 * @param array $request_items
670 * @param array $remove_rules
671 * @return array
672 */
673 public function is_remove_rule( $rule_id, $request_items, $remove_rules, $acf_post_types, $cptui_post_types ): array {
674 $is_rule_removed = false;
675 $modify_remove_variables = array();
676
677 if ( isset( $remove_rules['woocommerce'] ) ) {
678 if ( in_array( $rule_id, $remove_rules['woocommerce'], true ) ) {
679 $sbjs_cookie_keys = preg_grep( '/^sbjs_.+/', array_keys( $request_items['request_cookies'] ) );
680
681 // sourcebusterの除外(woocommerce)
682 if ( ! empty( $sbjs_cookie_keys ) ) {
683 foreach ( $sbjs_cookie_keys as $key ) {
684 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 ) {
685 $is_rule_removed = true;
686 break;
687 }
688 }
689 }
690 }
691 }
692
693 if ( isset( $remove_rules['ajax_customize'] ) ) {
694 // カスタマイズ操作、オートセーブ時の除外
695 if ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
696 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
697 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
698 $action = $request_items['args']['action'] ?? '';
699 if ( $action === 'update-widget' ) {
700 // actionがupdate-widgetの場合はルール�
701 �体を除外(cocoon)
702 $is_rule_removed = true;
703 } else {
704 // それ以外の場合はcustomized, customize_changeset_dataキーのみ除外
705 $modify_remove_variables['args'] = array( 'customized', 'customize_changeset_data' );
706 }
707 }
708 }
709 // ウィジェット保存時(cocoonテーマ)の除外
710 } elseif ( preg_match( '/wp-admin\/widgets\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
711 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
712 if ( ( $request_items['args']['action'] ?? '' ) === 'save-widget' ) {
713 $is_rule_removed = true;
714 }
715 }
716 // メニュー操作時(cocoonテーマ)の除外
717 } elseif ( preg_match( '/wp-admin\/nav-menus\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
718 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
719 $action = $request_items['args']['action'] ?? '';
720 if ( $action === 'update' || $action === 'edit' ) {
721 $is_rule_removed = true;
722 }
723 }
724 }
725 }
726
727 if ( isset( $remove_rules['rest_api'] ) ) {
728 $rest_endpoint = $this->get_rest_endpoint( $request_items );
729
730 // 投稿・編集(templates, blocks, template-parts, navigation, pages, posts)の場合はcontentキーを除外
731 if ( preg_match( '/templates|blocks|template-parts|navigation|pages|posts/', $rest_endpoint ) === 1 ) {
732 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
733 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
734 $modify_remove_variables['args'] = array( 'content' );
735 }
736 }
737
738 // global-styles の場合はstylesキーを除外
739 } elseif ( preg_match( '/global-styles/', $rest_endpoint ) === 1 ) {
740 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
741 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
742 $modify_remove_variables['args'] = array( '/^styles/' );
743 }
744 }
745
746 // batch の場合はrequestsキーを除外
747 } elseif ( preg_match( '/batch/', $rest_endpoint ) === 1 ) {
748 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
749 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
750 $modify_remove_variables['args'] = array( '/^requests/' );
751 }
752 }
753
754 // 投稿・編集の操作は特定のルールを除外する(post.php)
755 } elseif ( preg_match( '/wp-admin\/post\.php/', $request_items['request_filename'] ) === 1 ) {
756 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
757 $action = $request_items['args']['action'] ?? '';
758 if ( $action === 'editpost' ) {
759 $is_rule_removed = true;
760 } elseif ( $action === 'post-quickdraft-save' ) {
761 $modify_remove_variables['args'] = array( 'content' );
762 }
763 }
764
765 // カスタマイズ機能からの投稿作成時の除外
766 } elseif ( preg_match( '/wp-admin\/customize\.php|customize_changeset_uuid/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
767 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
768 if ( ( $request_items['args']['customize_autosaved'] ?? '' ) === 'on' || ( $request_items['args']['wp_customize'] ?? '' ) === 'on' ) {
769 $action = $request_items['args']['action'] ?? '';
770 if ( $action === 'customize-nav-menus-insert-auto-draft' ) {
771 $modify_remove_variables['args'] = array( '/^params/' );
772 }
773 }
774 }
775
776 // nishiki の場合はcontentキーを除外
777 } elseif ( preg_match( '/wp\/v2\/nishiki_pro_(patterns|content)/', $rest_endpoint ) === 1 ) {
778 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
779 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
780 $modify_remove_variables['args'] = array( 'content' );
781 }
782 }
783
784 // xerite の場合はcontentキーを除外
785 } elseif ( preg_match( '/wp\/v2\/xw_block_patterns/', $rest_endpoint ) === 1 ) {
786 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
787 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
788 $modify_remove_variables['args'] = array( 'content' );
789 }
790 }
791
792 // Lightning の場合はcontentキーを除外
793 } elseif ( preg_match( '/wp\/v2\/(cta|vk-block-patterns)/', $rest_endpoint ) === 1 ) {
794 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
795 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
796 $modify_remove_variables['args'] = array( 'content' );
797 }
798 }
799
800 // SWELL の場合はcontentキーを除外
801 } elseif ( preg_match( '/wp\/v2\/(lp|blog_parts)/', $rest_endpoint ) === 1 ) {
802 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
803 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
804 $modify_remove_variables['args'] = array( 'content' );
805 }
806 }
807
808 // Snow Monkey の場合はcontentキーを除外
809 } elseif ( preg_match( '/wp\/v2\/snow-monkey-search/', $rest_endpoint ) === 1 ) {
810 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
811 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
812 $modify_remove_variables['args'] = array( 'content' );
813 }
814 }
815
816 // Advanced Custom Fields の場合はcontentキーを除外
817 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
818 } elseif ( ! empty( $acf_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
819 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
820 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
821 $modify_remove_variables['args'] = array( 'content' );
822 }
823 }
824
825 // Custom Post Type UI の場合はcontentキーを除外
826 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
827 } elseif ( ! empty( $cptui_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $cptui_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
828 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
829 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
830 $modify_remove_variables['args'] = array( 'content' );
831 }
832 }
833 }
834 }
835
836 // cocoonテーマでの除外処理(theme-func-text, theme-settings, theme-ranking, theme-affiliate-tag)
837 if ( isset( $remove_rules['cocoon'] ) ) {
838 $referer = $_SERVER['HTTP_REFERER'] ?? '';
839 if ( preg_match( '/wp-admin\/admin\.php\?page\=theme-(func-text|settings|ranking|affiliate-tag)/', $referer ) === 1 ) {
840 if ( in_array( $rule_id, $remove_rules['cocoon'], true ) ) {
841 $action = $request_items['args']['action'] ?? '';
842 if ( $action === 'new' || $action === 'edit' ) {
843 $is_rule_removed = true;
844 }
845
846 if ( isset( $request_items['args']['comment_information_message'] ) ) {
847 $is_rule_removed = true;
848 }
849 }
850 }
851 }
852
853 if ( isset( $remove_rules['emanon'] ) ) {
854 // emanonルール除外
855 if ( preg_match( '/wp-admin\/admin.php\?page\=emanon_setting_page/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
856 if ( in_array( $rule_id, $remove_rules['emanon'], true ) ) {
857 if ( ( $request_items['args']['action'] ?? '' ) === 'delete_transients_emanon_setting' ) {
858 $is_rule_removed = true;
859 }
860 }
861 }
862 }
863
864 if ( isset( $remove_rules['vkexunit'] ) ) {
865 // vkExUnitルール除外(メイン設定)
866 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_main_setting/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
867 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
868 if ( preg_match( '/page\=vkExUnit_main_setting/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
869 $is_rule_removed = true;
870 }
871 }
872 }
873
874 // vkExUnitルール除外(cssカスタマイズ)
875 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_css_customize/', $_SERVER['REQUEST_URI'] ?? '' ) === 1 ) {
876 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
877 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
878 if ( strpos( $wp_http_referer, '/wp-admin/admin.php?page=vkExUnit_css_customize' ) !== false ) {
879 $is_rule_removed = true;
880 }
881 }
882 }
883 }
884
885 if ( isset( $remove_rules['nishiki'] ) ) {
886 // nishikiルール除外
887 if ( preg_match( '/wp-admin\/admin.php\?page\=nishiki-pro-general\.php/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
888 if ( in_array( $rule_id, $remove_rules['nishiki'], true ) ) {
889 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
890 $is_rule_removed = true;
891 }
892 }
893 }
894 }
895
896 if ( isset( $remove_rules['swell'] ) ) {
897 // swellルール除外
898 if ( preg_match( '/wp-admin\/admin.php\?page\=swell_settings_editor/', $_SERVER['HTTP_REFERER'] ?? '' ) === 1 ) {
899 if ( in_array( $rule_id, $remove_rules['swell'], true ) ) {
900 if ( ( $request_items['args']['action'] ?? '' ) === 'update' ) {
901 $is_rule_removed = true;
902 }
903 }
904 }
905 }
906
907 if ( isset( $remove_rules['comment'] ) ) {
908 // コメント編集時の除外
909 if ( preg_match( '/wp-admin\/comment\.php/', $request_items['request_filename'] ?? '' ) === 1 ) {
910 if ( in_array( $rule_id, $remove_rules['comment'], true ) ) {
911 if ( ( $request_items['args']['action'] ?? '' ) === 'editedcomment' ) {
912 $is_rule_removed = true;
913 }
914 }
915 }
916 }
917
918 if ( isset( $remove_rules['ajax_editor'] ) ) {
919 // テーマ・プラグインエディターの操作時の除外
920 if ( preg_match( '/wp-admin\/admin-ajax\.php/', $request_items['request_filename'] ) === 1 ) {
921 if ( in_array( $rule_id, $remove_rules['ajax_editor'], true ) ) {
922 $wp_http_referer = $request_items['args']['_wp_http_referer'] ?? '';
923 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $wp_http_referer ) === 1 ) {
924 $is_rule_removed = true;
925 }
926
927 // オートセーブ時
928 $screen_id = $request_items['args']['screen_id'] ?? '';
929 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $screen_id ) === 1 ) {
930 $is_rule_removed = true;
931 }
932 }
933 }
934 }
935 return array(
936 'is_removed' => $is_rule_removed,
937 'modify_remove_variables' => $modify_remove_variables,
938 );
939 }
940
941 /**
942 * Advanced Custom Fieldsプラグイン除外対応
943 * 有効なカスタム投稿タイプキーを取得する
944 *
945 * @return array
946 */
947 public function get_acf_post_types(): array {
948 global $wpdb;
949 $active_plugins = get_option( 'active_plugins' );
950 $acf_post_types = array();
951
952 if ( is_array( $active_plugins ) && preg_match( '/advanced-custom-fields/', implode( ',', $active_plugins ) ) ) {
953 $results = $wpdb->get_results(
954 $wpdb->prepare(
955 "SELECT post_content
956 FROM {$wpdb->posts}
957 WHERE post_type = %s
958 AND post_status = %s",
959 'acf-post-type',
960 'publish'
961 )
962 );
963
964 if ( ! empty( $results ) ) {
965 foreach ( $results as $result ) {
966 $post_content = unserialize( $result->post_content, [ 'allowed_classes' => false ] );
967
968 if ( is_array( $post_content ) && isset( $post_content['post_type'] ) ) {
969 $acf_post_types[] = $post_content['post_type'];
970 }
971 }
972 }
973 }
974
975 return $acf_post_types;
976 }
977
978
979 /**
980 * Custom Post Type UIプラグイン除外対応
981 * 有効なカスタム投稿タイプキーを取得する
982 *
983 * @return array
984 */
985 public function get_cptui_post_types(): array {
986 $active_plugins = get_option( 'active_plugins' );
987 $cptui_post_types = array();
988
989 if ( is_array( $active_plugins ) && preg_match( '/custom-post-type-ui/', implode( ',', $active_plugins ) ) ) {
990 $cptui_data = get_option( 'cptui_post_types' );
991
992 if ( is_string( $cptui_data ) ) {
993 $cptui_data = unserialize( $cptui_data, [ 'allowed_classes' => false ] );
994 }
995
996 if ( is_array( $cptui_data ) ) {
997 foreach ( $cptui_data as $post_type ) {
998 if ( is_array( $post_type ) && isset( $post_type['name'] ) ) {
999 $cptui_post_types[] = $post_type['name'];
1000 }
1001 }
1002 }
1003 }
1004
1005 return $cptui_post_types;
1006 }
1007
1008
1009 /**
1010 * waf_engine
1011 *
1012 * @param array $waf_rules
1013 * @param array $locationmatch_rules
1014 * @param int $available_rules
1015 * @param array $remove_rules
1016 * @return array
1017 */
1018 public function waf_engine( $waf_rules, $locationmatch_rules, $available_rules, $remove_rules ): array {
1019 $request_items = $this->get_request_items();
1020
1021 $locationmatch_removed_rule_ids = $this->locationmatch_remove_rules( $locationmatch_rules, $_SERVER['REQUEST_URI'] ?? '' );
1022 $skip = 0;
1023 $skipafter = '';
1024 $chain_items = array();
1025 $tmp_match_results = array();
1026
1027 // Advanced Custom Fieldsプラグイン除外対応で追加
1028 $acf_post_types = $this->get_acf_post_types();
1029 // Custom Post Type UIプラグイン除外対応で追加
1030 $cptui_post_types = $this->get_cptui_post_types();
1031
1032 foreach ( $waf_rules as $waf_rule ) {
1033 // 前回マッチしたルールからskipの設定を引き継いでいる場合はスキップ
1034 if ( $this->is_skip_enabled( $skip ) ) {
1035 $skip--;
1036 continue;
1037 }
1038
1039 // 前回マッチしたルールからskipafterの設定を引き継いでいる場合は現在のルールIDと比較し、一致するまでスキップ
1040 // ルールIDの比較結果が同じの場合は、$skipafterを初期化して次のルールから判定を行う
1041 if ( $this->is_skipafter_enabled( $skipafter ) ) {
1042 if ( $skipafter !== $waf_rule['id'] ) {
1043 continue;
1044 } else {
1045 $skipafter = '';
1046 continue;
1047 }
1048 }
1049
1050 // LocationMatchによるルールの除外があるか確認。ある場合は現在のルールIDと比較し、一致する場合はスキップ
1051 if ( ! empty( $locationmatch_removed_rule_ids ) ) {
1052 if ( in_array( $waf_rule['id'], $locationmatch_removed_rule_ids, true ) ) {
1053 continue;
1054 }
1055 }
1056
1057 // ルールにvariables設定がない場合、skip,akipafterの設定を確認して次のルール判定へ
1058 if ( empty( $waf_rule['variables'] ) ) {
1059 $skip = $this->check_skip( $waf_rule['skip'] );
1060 $skipafter = $this->check_skipafter( $waf_rule['skipafter'] );
1061 continue;
1062 }
1063
1064 // 特定の操作の場合、特定のルールを除外する
1065 $remove_rule_result = $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types, $cptui_post_types );
1066
1067 if ( $remove_rule_result['is_removed'] ) {
1068 continue;
1069 }
1070
1071 // ルールのremove_variablesを動的に変更
1072 if ( ! empty( $remove_rule_result['modify_remove_variables'] ) ) {
1073 $waf_rule['remove_variables'] = array_merge_recursive(
1074 $waf_rule['remove_variables'],
1075 $remove_rule_result['modify_remove_variables']
1076 );
1077 }
1078
1079 foreach ( $waf_rule['variables'] as $variable ) {
1080 switch ( $variable ) {
1081 case self::VARIABLE_ARGS:
1082 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS, $chain_items );
1083 break;
1084
1085 case self::VARIABLE_ARGS_NAMES:
1086 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_NAMES, $chain_items );
1087 break;
1088
1089 case self::VARIABLE_REQUEST_COOKIES:
1090 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES, $chain_items );
1091 break;
1092
1093 case self::VARIABLE_REQUEST_COOKIES_NAMES:
1094 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES_NAMES, $chain_items );
1095 break;
1096
1097 case self::VARIABLE_REQUEST_HEADERS:
1098 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_HEADERS, $chain_items );
1099 break;
1100
1101 case self::VARIABLE_REQUEST_FILENAME:
1102 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_REQUEST_FILENAME, $chain_items );
1103 break;
1104
1105 case self::VARIABLE_XML:
1106 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_XML, $chain_items );
1107 break;
1108 }
1109
1110 $is_matched = $results['is_matched'];
1111
1112 if ( $is_matched ) {
1113 $skip = $results['skip'];
1114 $skipafter = $results['skipafter'];
1115 $chain_items = $results['chain_items'];
1116 $match_results = $results['match_results'];
1117
1118 // マッチしたが、chain,skip,skipafterの設定がある場合は次のルール判定へ
1119 if ( ! empty( $chain_items ) || 0 < $skip || ! empty( $skipafter ) ) {
1120 break;
1121 }
1122
1123 // マッチしたが、除外設定されているルールの場合は値を保持して次のルール判定へ
1124 if ( ( $waf_rule['attack'] & $available_rules ) === 0 ) {
1125 $tmp_match_results = $match_results;
1126 break;
1127 }
1128
1129 // マッチした結果がある場合は終了処理へ(ログ記述、通知、画面表示)
1130 if ( ! empty( $match_results ) ) {
1131 $match_results['is_deny'] = true;
1132 $match_results['is_write_log'] = true;
1133
1134 return $match_results;
1135 }
1136 }
1137
1138 $chain_items = array();
1139 }
1140 }
1141
1142 if ( ! empty( $tmp_match_results ) ) {
1143 $match_results = $tmp_match_results;
1144 $match_results['is_deny'] = false;
1145 $match_results['is_write_log'] = true;
1146
1147 } else {
1148 $match_results['is_deny'] = false;
1149 $match_results['is_write_log'] = false;
1150 }
1151
1152 return $match_results;
1153 }
1154 }
1155