PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.14
CloudSecure WP Security v1.4.14
1.4.14 1.4.13 1.4.12 1.4.11 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 2 weeks ago cli 2 weeks ago lib 1 month ago captcha.php 2 weeks ago cloudsecure-wp.php 6 days ago common.php 4 months ago config.php 2 years ago disable-access-system-file.php 1 month ago disable-author-query.php 2 weeks ago disable-login.php 1 month ago disable-restapi.php 2 weeks ago disable-xmlrpc.php 1 year ago htaccess.php 4 months ago login-log.php 1 month ago login-notification.php 3 months ago protect-rest-batch.php 1 month ago rename-login-page.php 4 months ago restrict-admin-page.php 3 months ago server-error-notification.php 3 months ago two-factor-authentication.php 6 days ago unify-messages.php 2 years ago update-notice.php 9 months ago waf-engine.php 6 days ago waf.php 1 month ago
waf-engine.php
1636 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_GET = 'args_get';
9 protected const VARIABLE_ARGS_GET_NAMES = 'args_get_names';
10 protected const VARIABLE_ARGS_POST = 'args_post';
11 protected const VARIABLE_ARGS_POST_NAMES = 'args_post_names';
12 protected const VARIABLE_REQUEST_COOKIES = 'request_cookies';
13 protected const VARIABLE_REQUEST_COOKIES_NAMES = 'request_cookies_names';
14 protected const VARIABLE_REQUEST_HEADERS = 'request_headers';
15 protected const VARIABLE_REQUEST_FILENAME = 'request_filename';
16 protected const VARIABLE_XML = 'xml';
17 private $parsed_xml;
18 private $request_body = null;
19 private $excluded_user_by_cookie = null;
20
21
22
23 function __construct( array $info ) {
24 parent::__construct( $info );
25
26 }
27
28
29 /**
30 * リクエストボディ(php://input)を取得する
31 * 同一リクエスト�
32 でボディは不変のため、初回読み込み時にキャッシュして再読み込みを防ぐ
33 *
34 * @return string
35 */
36 private function get_request_body(): string {
37 if ( null === $this->request_body ) {
38 $this->request_body = (string) file_get_contents( 'php://input' );
39 }
40
41 return $this->request_body;
42 }
43
44
45 /**
46 * XML パース時のコールバック関数
47 *
48 * @return void
49 */
50 public function char( $parser, $data ): void {
51 $this->parsed_xml .= $data;
52 }
53
54
55 /**
56 * XML パース
57 *
58 * @return string
59 */
60 public function xml_parser(): string {
61 $request_body = $this->get_request_body();
62 $this->parsed_xml = '';
63
64 if ( ! empty( $request_body ) ) {
65 $parser = xml_parser_create();
66
67 xml_set_object( $parser, $this );
68 xml_set_character_data_handler( $parser, 'char' );
69
70 if ( ! xml_parse( $parser, $request_body ) ) {
71 $this->parsed_xml .= 'xml_parse_failed';
72 };
73
74 xml_parser_free( $parser );
75 }
76
77 return $this->parsed_xml;
78 }
79
80
81 /**
82 * ARGS_GET、ARGS_GET_NAMES 用 GETデータ取得
83 *
84 * @return array
85 */
86 public function get_request_args_get_data(): array {
87 if ( ! empty( $_GET ) ) {
88 return $_GET;
89 }
90
91 return array();
92 }
93
94
95 /**
96 * ARGS_POST、ARGS_POST_NAMES 用 POSTデータ取得
97 *
98 * @return array
99 */
100 public function get_request_args_post_data(): array {
101 if ( ! empty( $_POST ) ) {
102 return $_POST;
103 }
104
105 $post_data = $this->get_request_body();
106
107 if ( ! empty( $post_data ) ) {
108 $json_decoded_post_data = json_decode( $post_data, true );
109
110 // nullでなければjsonとなる
111 if ( is_array( $json_decoded_post_data ) ) {
112 return $json_decoded_post_data;
113 }
114 }
115
116 return array();
117 }
118
119
120 /**
121 * Cookie�
122 報取得
123 *
124 * @return array
125 */
126 public function get_request_cookies(): array {
127 if ( ! empty( $_COOKIE ) ) {
128 return $_COOKIE;
129 } else {
130 return array();
131 }
132 }
133
134
135 /**
136 * リクエスト�
137 報取得
138 * 取得・パースできなかったものに関しては空白で返す
139 *
140 * @return array
141 */
142 public function get_request_items(): array {
143 $request_items = array(
144 'access_at' => current_time( 'mysql' ),
145 'ip' => $this->get_client_ip(),
146 self::VARIABLE_XML => $this->xml_parser(),
147 self::VARIABLE_REQUEST_FILENAME => $this->get_request_uri(),
148 self::VARIABLE_REQUEST_HEADERS => $this->get_http_request_headers(),
149 self::VARIABLE_ARGS_GET => $this->get_request_args_get_data(),
150 self::VARIABLE_ARGS_POST => $this->get_request_args_post_data(),
151 self::VARIABLE_REQUEST_COOKIES => $this->get_request_cookies(),
152 );
153
154 return $request_items;
155 }
156
157
158 /**
159 * ルールの設定を参考に1つのリクエスト�
160 報に対して複数の変換を行う処理
161 *
162 * @param array $transformations
163 * @param string $request_item
164 * @return string
165 */
166 public function transform( $transformations, $request_item ): string {
167 $converted_request_item = $request_item;
168
169 foreach ( $transformations as $transformation ) {
170 switch ( $transformation ) {
171 case 'htmlentitydecode':
172 $converted_request_item = html_entity_decode( $converted_request_item, ENT_QUOTES, 'utf-8' );
173 break;
174 case 'lowercase':
175 $converted_request_item = strtolower( $converted_request_item );
176 break;
177 case 'replacecomments':
178 $converted_request_item = preg_replace( '/(\/\*.*?\*\/|\/\*.*(?!\*\/).*)/s', ' ', $converted_request_item );
179 break;
180 case 'compresswhitespace':
181 $converted_request_item = preg_replace( '/(\s|\xa0)/s', ' ', $converted_request_item );
182 $converted_request_item = preg_replace( '/\s{2,}/s', ' ', $converted_request_item );
183 break;
184 default:
185 $converted_request_item = '変換に失敗しました';
186 break 2;
187 }
188 }
189
190 return $converted_request_item;
191 }
192
193
194 /**
195 * LocationMatch設定によるルールのスキップ用関数
196 *
197 * @param array $locationmatch_rules
198 * @param string $request_uri
199 * @return array
200 */
201 public function locationmatch_remove_rules( $locationmatch_rules, $request_uri ): array {
202 $locationmatch_removed_rule_ids = array();
203
204 foreach ( $locationmatch_rules as $locationmatch_rule ) {
205 if ( preg_match( '/' . $locationmatch_rule['path'] . '/', $request_uri ) ) {
206 $locationmatch_removed_rule_ids = array_merge( $locationmatch_removed_rule_ids, $locationmatch_rule['remove_rule_ids'] );
207 }
208 }
209
210 $locationmatch_removed_rule_ids = array_unique( $locationmatch_removed_rule_ids );
211
212 return $locationmatch_removed_rule_ids;
213 }
214
215
216 /**
217 * skip表記の存在確認
218 *
219 * @param int $skip
220 * @return int
221 */
222 public function check_skip( $skip ): int {
223 if ( $skip !== 0 ) {
224 return $skip;
225 } else {
226 return 0;
227 }
228 }
229
230
231 /**
232 * skipafter表記の存在確認
233 *
234 * @param string $skipafter
235 * @return string
236 */
237 public function check_skipafter( $skipafter ): string {
238 if ( ! empty( $skipafter ) ) {
239 return $skipafter;
240 } else {
241 return '';
242 }
243 }
244
245
246 /**
247 * skipが有効か判定
248 *
249 * @param int $skip
250 * @return bool
251 */
252 public function is_skip_enabled( $skip ): bool {
253 if ( 0 < $skip ) {
254 return true;
255 } else {
256 return false;
257 }
258 }
259
260
261 /**
262 * skipafterが有効か判定
263 *
264 * @param string $skipafter
265 * @return bool
266 */
267 public function is_skipafter_enabled( $skipafter ): bool {
268 if ( ! empty( $skipafter ) ) {
269 return true;
270 } else {
271 return false;
272 }
273 }
274
275 /**
276 * ルールが終了したか判定
277 *
278 * @param string $chain
279 * @param string $skip
280 * @param string $skipafter
281 * @return bool
282 */
283 public function is_rule_finished( $chain, $skip, $skipafter ): bool {
284 if ( ! empty( $chain ) || $this->is_skip_enabled( $skip ) || $this->is_skipafter_enabled( $skipafter ) ) {
285 return false;
286 } else {
287 return true;
288 }
289 }
290
291
292 /**
293 * ルール�
294 に除外の表記があるか確認
295 *
296 * @param array $remove_variables
297 * @param string $variable
298 * @param string $request_item
299 * @return bool
300 */
301 public function is_remove_variables( $remove_variables, $variable, $request_item ): bool {
302 if ( ! empty( $remove_variables ) ) {
303 foreach ( $remove_variables as $remove_variable => $remove_values ) {
304 if ( $remove_variable === $variable ) {
305 foreach ( $remove_values as $remove_value ) {
306
307 // REQUEST_HEADERSの場合は大文字小文字関係なく判定する
308 if ( $variable === self::VARIABLE_REQUEST_HEADERS ) {
309 if ( preg_match( '/' . $remove_value . '/i', $request_item ) ) {
310 return true;
311 }
312 }
313
314 if ( preg_match( '/^\/.+\/$/', $remove_value ) ) {
315 // 除外の値が部分一致(/で囲まれているもの)の場合
316 if ( preg_match( $remove_value . 's', $request_item ) ) {
317 return true;
318 }
319 } else {
320 // 除外の値が完�
321 �一致の場合
322 if ( $remove_value === $request_item ) {
323 return true;
324 }
325 }
326 }
327 }
328 }
329 }
330
331 return false;
332 }
333
334 /**
335 * マッチした結果を�
336 �列に格納
337 *
338 * @param array $rule
339 * @param array $request_items
340 * @param string $variable
341 * @param string $match_string
342 * @return array
343 */
344 public function get_match_results( $rule, $request_items, $variable, $match_string ): array {
345
346 if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
347 $complete_url = ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' )
348 . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) )
349 . esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
350 } else {
351 $complete_url = '';
352 }
353
354 $match_results = array(
355 'id' => $rule['id'],
356 'attack' => $rule['attack'],
357 'variable' => $variable,
358 'matched' => $match_string,
359 'ip' => $request_items['ip'],
360 'access_at' => $request_items['access_at'],
361 'url' => $complete_url,
362 );
363
364 return $match_results;
365 }
366
367
368 /**
369 * chain_itemsの取得
370 *
371 * @param array $rule
372 * @return array
373 */
374 public function get_chain_items( $rule ): array {
375 $chain_items = array(
376 'id' => $rule['id'],
377 'attack' => $rule['attack'],
378 'skip' => $rule['skip'],
379 'skipafter' => $rule['skipafter'],
380 );
381
382 return $chain_items;
383 }
384
385
386 /**
387 * マッチしたルールの設定、結果を取得
388 *
389 * @param array $waf_rule
390 * @param array $request_items
391 * @param string $variable
392 * @param array $chain_items
393 * @param array $match_string
394 * @return array
395 */
396 public function get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $match_string ): array {
397 $results = array(
398 'is_matched' => true,
399 'chain_items' => array(),
400 'skip' => 0,
401 'skipafter' => '',
402 'match_results' => array(),
403 );
404
405 // 前ルールからchainの設定を引き継いでいるか確認
406 if ( ! empty( $chain_items ) ) {
407 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
408 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
409 $results['is_matched'] = false;
410
411 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $chain_items['skip'], $chain_items['skipafter'] ) ) {
412 $results['match_results'] = $this->get_match_results( $chain_items, $request_items, $variable, $match_string );
413
414 } elseif ( $waf_rule['chain'] ) {
415 // 現在のルールにchainの設定がある場合
416 $results['chain_items'] = $chain_items;
417
418 } else {
419 $results['skip'] = $this->check_skip( $chain_items['skip'] );
420 $results['skipafter'] = $this->check_skipafter( $chain_items['skipafter'] );
421 }
422 } else {
423 if ( $waf_rule['attack'] === '' && $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
424 // ルールの終了判定がtrueではあるが、攻撃種別が設定されていない場合はマッチ用のルールではないのでマッチしていないと判定
425 $results['is_matched'] = false;
426
427 } elseif ( $this->is_rule_finished( $waf_rule['chain'], $waf_rule['skip'], $waf_rule['skipafter'] ) ) {
428 $results['match_results'] = $this->get_match_results( $waf_rule, $request_items, $variable, $match_string );
429
430 } elseif ( $waf_rule['chain'] ) {
431 // 現在のルールにchainの設定がある場合
432 $results['chain_items'] = $this->get_chain_items( $waf_rule );
433
434 } else {
435 $results['skip'] = $this->check_skip( $waf_rule['skip'] );
436 $results['skipafter'] = $this->check_skipafter( $waf_rule['skipafter'] );
437 }
438 }
439 return $results;
440 }
441
442 /**
443 * ネストした�
444 �列をパース
445 *
446 * @param string $name
447 * @param array $array
448 */
449 public function parse_array( $name, $array ) {
450 $parsed_array = array();
451
452 foreach ( $array as $key => $val ) {
453 if ( is_array( $val ) ) {
454 $tmp_name = $name . '[' . $key . ']';
455 $tmp_parsed_array = $this->parse_array( $tmp_name, $val );
456 $parsed_array = array_merge( $parsed_array, $tmp_parsed_array );
457
458 } else {
459 $parsed_array_key = $name . '[' . $key . ']';
460 $parsed_array[ $parsed_array_key ] = $val ?? '';
461 }
462 }
463 return $parsed_array;
464 }
465
466
467 /**
468 * リクエスト�
469 報�
470 �列を使用するルール判定
471 *
472 * @param array $waf_rule
473 * @param array $request_items
474 * @param string $variable
475 * @param array $chain_items
476 * @return array
477 */
478 public function check_request_item_array( $waf_rule, $request_items, $variable, $chain_items ) {
479 $results = array(
480 'is_matched' => false,
481 'chain_items' => array(),
482 'skip' => 0,
483 'skipafter' => '',
484 'match_results' => array(),
485 'has_backtrack_error' => false,
486 'backtrack_key' => '',
487 );
488
489 $get_request_item_variable = preg_replace( '/_names/', '', $variable );
490
491 if ( ! isset( $get_request_item_variable ) ) {
492 return $results;
493 }
494
495 foreach ( $request_items[ $get_request_item_variable ] as $key => $val ) {
496
497 if ( ! isset( $val ) ) {
498 $val = '';
499 }
500
501 switch ( $variable ) {
502 case self::VARIABLE_ARGS_GET:
503 case self::VARIABLE_ARGS_POST:
504 case self::VARIABLE_REQUEST_COOKIES:
505 case self::VARIABLE_REQUEST_HEADERS:
506 if ( is_array( $val ) ) {
507 $parsed_vals = $this->parse_array( $key, $val );
508
509 foreach ( $parsed_vals as $key => $val ) {
510 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
511
512 if ( $checked_request_item['has_backtrack_error'] ) {
513 $results['has_backtrack_error'] = true;
514 $results['backtrack_key'] = $checked_request_item['backtrack_key'] ?? '';
515 }
516
517 if ( $checked_request_item['is_matched'] ) {
518 $has_bt = $results['has_backtrack_error'];
519 $has_btk = $results['backtrack_key'];
520 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
521
522 $results['has_backtrack_error'] = $has_bt;
523 $results['backtrack_key'] = $has_btk;
524 break;
525 }
526 }
527 } else {
528 $checked_request_item = $this->check_request_item_value( $waf_rule, $key, $val, $variable );
529 }
530
531 break;
532
533 case self::VARIABLE_ARGS_GET_NAMES:
534 case self::VARIABLE_ARGS_POST_NAMES:
535 case self::VARIABLE_REQUEST_COOKIES_NAMES:
536 $checked_request_item = $this->check_request_item_key( $waf_rule, $key, $variable );
537 break;
538 }
539
540 if ( isset( $checked_request_item['has_backtrack_error'] ) && $checked_request_item['has_backtrack_error'] ) {
541 $results['has_backtrack_error'] = true;
542 $results['backtrack_key'] = $checked_request_item['backtrack_key'] ?? '';
543 }
544
545 if ( $checked_request_item['is_matched'] ) {
546 $has_bt = $results['has_backtrack_error'];
547 $has_btk = $results['backtrack_key'];
548 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $checked_request_item['match_string'] );
549
550 $results['has_backtrack_error'] = $has_bt;
551 $results['backtrack_key'] = $has_btk;
552
553 break;
554 }
555 }
556 return $results;
557 }
558
559
560
561 /**
562 * リクエスト�
563 報�
564 �列のうちvalueの値を使用するルール判定
565 *
566 * @param array $waf_rule
567 * @param string $request_items_key
568 * @param string $request_items_value
569 * @param string $variable
570 * @return array
571 */
572 public function check_request_item_value( $waf_rule, $request_items_key, $request_items_value, $variable ): array {
573 $results = array(
574 'is_matched' => false,
575 'match_string' => '',
576 'has_backtrack_error' => false,
577 'backtrack_key' => '',
578 );
579 $matches = array();
580
581 // リクエスト�
582 報�
583 �列のkeyの変換
584 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
585
586 // 除外対象のキーは検査自体を行わない
587 // preg_matchを�
588 �に実行するとバックトラック�
589 過時に正常な操作が誤って遮断されるため、除外判定を検査前に行う
590 if ( true === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
591 return $results;
592 }
593
594 // リクエスト�
595 報�
596 �列のvalueの変換
597 $tmp_val = $this->transform( $waf_rule['transformations'], $request_items_value );
598
599 $preg_result = preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_val, $matches );
600
601 if ( $preg_result === false ) {
602 // preg_matchが完走しなかった場合(バックトラック/JITスタック/再帰上限/不正UTF-8等)は検査失敗として扱う
603 $results['has_backtrack_error'] = true;
604 $results['backtrack_key'] = $request_items_key;
605 } elseif ( $preg_result === 1 ) {
606 if ( ! empty( $matches ) ) {
607 // 除外判定は検査前に実施済みのためマッチしたと判定
608 $results['is_matched'] = true;
609 $results['match_string'] = $matches[0];
610 }
611 }
612
613 return $results;
614 }
615
616
617 /**
618 * リクエスト�
619 報�
620 �列のうちkeyの値を使用するルール判定
621 *
622 * @param array $waf_rule
623 * @param string $request_items_key
624 * @param string $variable
625 * @return array
626 */
627 public function check_request_item_key( $waf_rule, $request_items_key, $variable ): array {
628 $results = array(
629 'is_matched' => false,
630 'match_string' => '',
631 'has_backtrack_error' => false,
632 'backtrack_key' => '',
633 );
634 $matches = array();
635
636 // リクエスト�
637 報�
638 �列のkeyの変換
639 $tmp_key = $this->transform( $waf_rule['transformations'], $request_items_key );
640
641 // 除外対象のキーは検査自体を行わない
642 // preg_matchを�
643 �に実行するとバックトラック�
644 過時に正常な操作が誤って遮断されるため、除外判定を検査前に行う
645 if ( true === $this->is_remove_variables( $waf_rule['remove_variables'], $variable, $tmp_key ) ) {
646 return $results;
647 }
648
649 $preg_result = preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_key, $matches );
650
651 if ( $preg_result === false ) {
652 // preg_matchが完走しなかった場合(バックトラック/JITスタック/再帰上限/不正UTF-8等)は検査失敗として扱う
653 $results['has_backtrack_error'] = true;
654 $results['backtrack_key'] = $request_items_key;
655 } elseif ( $preg_result === 1 ) {
656 if ( ! empty( $matches ) ) {
657 // 除外判定は検査前に実施済みのためマッチしたと判定
658 $results['is_matched'] = true;
659 $results['match_string'] = $matches[0];
660 }
661 }
662
663 return $results;
664 }
665
666
667 /**
668 * リクエスト�
669 報�
670 �列のうち文字列の値を使用するルール判定
671 *
672 * @param array $waf_rule
673 * @param array $request_items
674 * @param string $variable
675 * @param array $chain_items
676 * @return array
677 */
678 public function check_request_item_strings( $waf_rule, $request_items, $variable, $chain_items ): array {
679 $results = array(
680 'is_matched' => false,
681 'chain_items' => array(),
682 'skip' => 0,
683 'skipafter' => '',
684 'match_results' => array(),
685 'has_backtrack_error' => false,
686 );
687
688 if ( empty( $request_items[ $variable ] ) ) {
689 return $results;
690 }
691
692 if ( $variable === self::VARIABLE_XML ) {
693 if ( $request_items[ self::VARIABLE_XML ] === 'xml_parse_failed' ) {
694 return $results;
695 } else {
696 $request_items[ self::VARIABLE_XML ] = str_replace( 'xml_parse_failed', '', $request_items[ self::VARIABLE_XML ] );
697 }
698 }
699
700 // リクエスト�
701 報の変換
702 $tmp_string = $this->transform( $waf_rule['transformations'], $request_items[ $variable ] );
703
704 // REQUEST_FILENAMEに関しては、urlデコード済の値も判定し、マッチしたら結果を出力
705 if ( $variable === self::VARIABLE_REQUEST_FILENAME ) {
706 // リクエスト�
707 報のデコード&変換
708 $request_items_urldecoded = urldecode( $request_items[ $variable ] );
709 $tmp_urldecoded = $this->transform( $waf_rule['transformations'], $request_items_urldecoded );
710
711 $preg_result = preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_urldecoded, $matches );
712
713 if ( $preg_result === false ) {
714 // preg_matchが完走しなかった場合(バックトラック/JITスタック/再帰上限/不正UTF-8等)は検査失敗として扱う
715 $results['has_backtrack_error'] = true;
716 } elseif ( $preg_result === 1 ) {
717 if ( ! empty( $matches ) ) {
718 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
719 return $results;
720 }
721 }
722 }
723
724 $preg_result = preg_match( '/' . $waf_rule['regex_pattern'] . '/s', $tmp_string, $matches );
725
726 if ( $preg_result === false ) {
727 // preg_matchが完走しなかった場合(バックトラック/JITスタック/再帰上限/不正UTF-8等)は検査失敗として扱う
728 $results['has_backtrack_error'] = true;
729 return $results;
730 }
731
732 if ( empty( $matches ) ) {
733 // 正規表現パターンにマッチしなければ終了
734 return $results;
735 }
736
737 $has_bt = $results['has_backtrack_error'];
738 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, $matches[0] );
739
740 $results['has_backtrack_error'] = $has_bt;
741
742 return $results;
743 }
744
745
746 /**
747 * REST APIエンドポイントを取得(パーマリンク設定に依存しない)
748 *
749 * @param array $request_items
750 * @return string
751 */
752 public function get_rest_endpoint( $request_items ): string {
753 // パーマリンクが「基本」の場合はrest_routeパラメータにエンドポイントが含まれる
754 // GETとPOSTの両方にrest_routeが含まれる場合は404エラーになる
755 if ( isset( $request_items['args_get']['rest_route'] ) && is_string( $request_items['args_get']['rest_route'] ) ) {
756 return $request_items['args_get']['rest_route'];
757 }
758 if ( isset( $request_items['args_post']['rest_route'] ) && is_string( $request_items['args_post']['rest_route'] ) ) {
759 return $request_items['args_post']['rest_route'];
760 }
761
762 // その他のパーマリンク設定の場合はrequest_filenameを使用
763 return $request_items['request_filename'];
764 }
765
766
767 /**
768 * AND判定の�
769 �通ロジック
770 *
771 * 存在するチャネル(args_get / args_post)の値がすべて $predicate を満たし、かつ最低1つのチャネルに存在する場合のみ true。
772 *
773 * @param array $request_items
774 * @param string $key
775 * @param callable $predicate function( mixed $value ): bool 1チャネルの値が条件を満たすか
776 * @return bool
777 */
778 private function args_value_all_match( array $request_items, string $key, callable $predicate ): bool {
779 $args_get = $request_items['args_get'] ?? array();
780 $args_post = $request_items['args_post'] ?? array();
781
782 $in_args_get = array_key_exists( $key, $args_get );
783 $in_args_post = array_key_exists( $key, $args_post );
784
785 if ( ! $in_args_get && ! $in_args_post ) {
786 return false;
787 }
788 if ( $in_args_get && ! $predicate( $args_get[ $key ] ) ) {
789 return false;
790 }
791 if ( $in_args_post && ! $predicate( $args_post[ $key ] ) ) {
792 return false;
793 }
794
795 return true;
796 }
797
798
799 /**
800 * 判定対象キーに対する値の完�
801 �一致判定(AND判定)
802 * 許可する値が1つの場合
803 *
804 * 存在するチャネルの値がすべて $expected と厳密一致し、かつ最低1つのチャネルに存在する場合のみ true。
805 *
806 * @param array $request_items
807 * @param string $key
808 * @param string $expected
809 * @return bool
810 */
811 private function args_value_matches( array $request_items, string $key, string $expected ): bool {
812 return $this->args_value_all_match(
813 $request_items,
814 $key,
815 static function ( $value ) use ( $expected ) {
816 return $value === $expected;
817 }
818 );
819 }
820
821
822 /**
823 * 判定対象キーに対する値の完�
824 �一致判定(AND判定)
825 * 許可する値が2つ以上の場合
826 *
827 * 存在するチャネルの値がすべて $allowed のいずれかと厳密一致し、かつ最低1つのチャネルに存在する場合のみ true。
828 *
829 * @param array $request_items
830 * @param string $key
831 * @param string[] $allowed
832 * @return bool
833 */
834 private function args_value_matches_any( array $request_items, string $key, array $allowed ): bool {
835 return $this->args_value_all_match(
836 $request_items,
837 $key,
838 static function ( $value ) use ( $allowed ) {
839 return in_array( $value, $allowed, true );
840 }
841 );
842 }
843
844
845 /**
846 * 判定対象キーに対する値の正規表現一致判定(AND判定)
847 *
848 * 存在するチャネルの値がすべて $pattern に一致し、かつ最低1つに存在する場合のみ true。
849 *
850 * @param array $request_items
851 * @param string $key
852 * @param string $pattern
853 * @return bool
854 */
855 private function args_value_matches_regex( array $request_items, string $key, string $pattern ): bool {
856 return $this->args_value_all_match(
857 $request_items,
858 $key,
859 static function ( $value ) use ( $pattern ) {
860 return is_string( $value ) && 1 === preg_match( $pattern, $value );
861 }
862 );
863 }
864
865
866 /**
867 * 判定対象キーに対する値の部分一致判定(AND判定)
868 *
869 * 存在するチャネルの値がすべて $needle を含み、かつ最低1つに存在する場合のみ true。
870 *
871 * @param array $request_items
872 * @param string $key
873 * @param string $needle
874 * @return bool
875 */
876 private function args_value_matches_partial( array $request_items, string $key, string $needle ): bool {
877 return $this->args_value_all_match(
878 $request_items,
879 $key,
880 static function ( $value ) use ( $needle ) {
881 return is_string( $value ) && false !== strpos( $value, $needle );
882 }
883 );
884 }
885
886
887 /**
888 * 判定対象キーの存在判定(OR判定)
889 *
890 * どちらかのチャネルに存在すれば true
891 *
892 * @param array $request_items
893 * @param string $key
894 * @return bool
895 */
896 private function args_key_exists( array $request_items, string $key ): bool {
897 $args_get = $request_items['args_get'] ?? array();
898 $args_post = $request_items['args_post'] ?? array();
899
900 return array_key_exists( $key, $args_get ) || array_key_exists( $key, $args_post );
901 }
902
903
904 /**
905 * WordPress管理画面での特定の処理に対し、特定のルールを除外する
906 *
907 * @param string $rule_id
908 * @param array $request_items
909 * @param array $remove_rules
910 * @return array
911 */
912 public function is_remove_rule( $rule_id, $request_items, $remove_rules, $acf_post_types, $cptui_post_types ): array {
913 $is_rule_removed = false;
914 $modify_remove_variables = array();
915 $modify_variables = array();
916
917 if ( isset( $remove_rules['woocommerce'] ) ) {
918 if ( in_array( $rule_id, $remove_rules['woocommerce'], true ) ) {
919 $sbjs_cookie_keys = preg_grep( '/^sbjs_.+/', array_keys( $request_items['request_cookies'] ) );
920
921 // sourcebusterの除外(woocommerce)
922 if ( ! empty( $sbjs_cookie_keys ) ) {
923 foreach ( $sbjs_cookie_keys as $key ) {
924 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 ) {
925 $is_rule_removed = true;
926 break;
927 }
928 }
929 }
930 }
931 }
932
933 if ( isset( $remove_rules['ajax_customize'] ) ) {
934 if ( in_array( $rule_id, $remove_rules['ajax_customize'], true ) ) {
935 // カスタマイズ操作、オートセーブ時の除外(issetで�
936 �に判定、最安価)
937 if (
938 $this->args_key_exists( $request_items, 'customize_changeset_uuid' ) &&
939 ( $this->args_value_matches( $request_items, 'customize_autosaved', 'on' ) || $this->args_value_matches( $request_items, 'wp_customize', 'on' ) )
940 ) {
941 $action = $request_items['args_post']['action'] ?? '';
942 if ( $action === 'update-widget' ) {
943 // actionがupdate-widgetの場合はルール�
944 �体を除外(cocoon)
945 $is_rule_removed = true;
946 } else {
947 // それ以外の場合はcustomized, customize_changeset_dataキーのみ除外
948 $modify_remove_variables['args_post'] = array( 'customized', 'customize_changeset_data' );
949 }
950 // admin-ajax.phpへのリクエスト(ウィジェット保存・メニュー操作(WP5.3)をまとめて1回のpreg_matchで判定)
951 } elseif ( preg_match( '/wp-admin\/admin-ajax\.php/', $request_items['request_filename'] ) === 1 ) {
952 $action = $request_items['args_post']['action'] ?? '';
953 // ウィジェット保存時(cocoonテーマ)の除外
954 if ( $action === 'save-widget' && isset( $request_items['args_post']['widget-id'] ) ) {
955 $is_rule_removed = true;
956 // メニュー操作時(WordPress5.3)の除外
957 } elseif ( $action === 'add-menu-item' ) {
958 $is_rule_removed = true;
959 }
960 // メニュー操作時(cocoonテーマ)の除外
961 } elseif ( preg_match( '/wp-admin\/nav-menus\.php/', $request_items['request_filename'] ) === 1 ) {
962 if ( $this->args_value_matches_any( $request_items, 'action', array( 'update', 'edit' ) ) ) {
963 $is_rule_removed = true;
964 }
965 }
966 }
967 }
968
969 if ( isset( $remove_rules['rest_api'] ) ) {
970 $rest_endpoint = $this->get_rest_endpoint( $request_items );
971
972 // 投稿・編集(templates, blocks, template-parts, navigation, pages, posts)の場合はcontentキーを除外
973 if ( preg_match( '/templates|blocks|template-parts|navigation|pages|posts/', $rest_endpoint ) === 1 ) {
974 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
975 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
976 $modify_remove_variables['args_get'] = array( 'content' );
977 $modify_remove_variables['args_post'] = array( 'content' );
978 }
979 }
980
981 // global-styles の場合はstylesキーを除外
982 } elseif ( preg_match( '/global-styles/', $rest_endpoint ) === 1 ) {
983 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
984 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
985 $modify_remove_variables['args_get'] = array( '/^styles/' );
986 $modify_remove_variables['args_post'] = array( '/^styles/' );
987 }
988 }
989
990 // batch の場合はrequestsキーを除外
991 } elseif ( preg_match( '/batch/', $rest_endpoint ) === 1 ) {
992 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
993 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
994 $modify_remove_variables['args_post'] = array( '/^requests/' );
995 }
996 }
997
998 // 投稿・編集の操作は特定のルールを除外する(post.php)
999 } elseif ( preg_match( '/wp-admin\/post\.php/', $request_items['request_filename'] ) === 1 ) {
1000 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1001 if ( $this->args_value_matches( $request_items, 'action', 'editpost' ) ) {
1002 $is_rule_removed = true;
1003 } elseif ( $this->args_value_matches( $request_items, 'action', 'post-quickdraft-save' ) ) {
1004 $modify_remove_variables['args_post'] = array( 'content' );
1005 }
1006 }
1007
1008 // カスタマイズ機能からの投稿作成時の除外
1009 } elseif (
1010 $this->args_key_exists( $request_items, 'customize_changeset_uuid' ) &&
1011 ( $this->args_value_matches( $request_items, 'customize_autosaved', 'on' ) || $this->args_value_matches( $request_items, 'wp_customize', 'on' ) )
1012 ) {
1013 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1014 if ( $this->args_value_matches( $request_items, 'action', 'customize-nav-menus-insert-auto-draft' ) ) {
1015 $modify_remove_variables['args_post'] = array( '/^params/' );
1016 }
1017 }
1018
1019 // nishiki の場合はcontentキーを除外
1020 } elseif ( preg_match( '/wp\/v2\/nishiki_pro_(patterns|content)/', $rest_endpoint ) === 1 ) {
1021 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1022 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1023 $modify_remove_variables['args_get'] = array( 'content' );
1024 $modify_remove_variables['args_post'] = array( 'content' );
1025 }
1026 }
1027
1028 // xerite の場合はcontentキーを除外
1029 } elseif ( preg_match( '/wp\/v2\/xw_block_patterns/', $rest_endpoint ) === 1 ) {
1030 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1031 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1032 $modify_remove_variables['args_get'] = array( 'content' );
1033 $modify_remove_variables['args_post'] = array( 'content' );
1034 }
1035 }
1036
1037 // Lightning の場合はcontentキーを除外
1038 } elseif ( preg_match( '/wp\/v2\/(cta|vk-block-patterns)/', $rest_endpoint ) === 1 ) {
1039 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1040 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1041 $modify_remove_variables['args_get'] = array( 'content' );
1042 $modify_remove_variables['args_post'] = array( 'content' );
1043 }
1044 }
1045
1046 // SWELL の場合はcontentキーを除外
1047 } elseif ( preg_match( '/wp\/v2\/(lp|blog_parts)/', $rest_endpoint ) === 1 ) {
1048 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1049 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1050 $modify_remove_variables['args_get'] = array( 'content' );
1051 $modify_remove_variables['args_post'] = array( 'content' );
1052 }
1053 }
1054
1055 // Snow Monkey の場合はcontentキーを除外
1056 } elseif ( preg_match( '/wp\/v2\/snow-monkey-search/', $rest_endpoint ) === 1 ) {
1057 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1058 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1059 $modify_remove_variables['args_get'] = array( 'content' );
1060 $modify_remove_variables['args_post'] = array( 'content' );
1061 }
1062 }
1063
1064 // Advanced Custom Fields の場合はcontentキーを除外
1065 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
1066 } elseif ( ! empty( $acf_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $acf_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
1067 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1068 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1069 $modify_remove_variables['args_get'] = array( 'content' );
1070 $modify_remove_variables['args_post'] = array( 'content' );
1071 }
1072 }
1073
1074 // Custom Post Type UI の場合はcontentキーを除外
1075 // カスタム投稿タイプキーは小文字、アンダースコア、ダッシュのみを許容するが、念のためarray_mapで正規表現用にエスケープする
1076 } elseif ( ! empty( $cptui_post_types ) && preg_match( '/wp\/v2\/(' . implode( '|', array_map( 'preg_quote', $cptui_post_types ) ) . ')/', $rest_endpoint ) === 1 ) {
1077 if ( in_array( $rule_id, $remove_rules['rest_api'], true ) ) {
1078 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1079 $modify_remove_variables['args_get'] = array( 'content' );
1080 $modify_remove_variables['args_post'] = array( 'content' );
1081 }
1082 }
1083 }
1084 }
1085
1086 if ( isset( $remove_rules['rest_api_search'] ) ) {
1087 $rest_endpoint = $this->get_rest_endpoint( $request_items );
1088
1089 // pages または posts エンドポイントで context=edit かつ search パラメータが存在する場合は search の値を除外
1090 if ( preg_match( '/wp\/v2\/(pages|posts)/', $rest_endpoint ) === 1 ) {
1091 if ( in_array( $rule_id, $remove_rules['rest_api_search'], true ) ) {
1092 if ( preg_match( '/_locale\=user/', $_SERVER['QUERY_STRING'] ?? '' ) === 1 ) {
1093 if ( ( $request_items['args_get']['context'] ?? '' ) === 'edit' && isset( $request_items['args_get']['search'] ) ) {
1094 $modify_remove_variables['args_get'] = array( 'search' );
1095 }
1096 }
1097 }
1098 }
1099 }
1100
1101 // cocoonテーマでの除外処理(theme-func-text, theme-settings, theme-ranking, theme-affiliate-tag)
1102 if ( isset( $remove_rules['cocoon'] ) ) {
1103 $page = $request_items['args_get']['page'] ?? '';
1104 if ( preg_match( '/^theme-(func-text|settings|ranking|affiliate-tag)$/', $page ) === 1 ) {
1105 if ( preg_match( '/wp-admin\/admin\.php/', $request_items['request_filename'] ) === 1 ) {
1106 if ( in_array( $rule_id, $remove_rules['cocoon'], true ) ) {
1107 if ( $this->args_value_matches_any( $request_items, 'action', array( 'new', 'edit' ) ) ) {
1108 $is_rule_removed = true;
1109 }
1110
1111 if ( isset( $request_items['args_post']['comment_information_message'] ) ) {
1112 $is_rule_removed = true;
1113 }
1114 }
1115 }
1116 }
1117 }
1118
1119 if ( isset( $remove_rules['emanon'] ) ) {
1120 // emanonルール除外
1121 if ( ( $request_items['args_get']['page'] ?? '' ) === 'emanon_setting_page' ) {
1122 if ( preg_match( '/wp-admin\/admin\.php/', $request_items['request_filename'] ) === 1 ) {
1123 if ( in_array( $rule_id, $remove_rules['emanon'], true ) ) {
1124 if ( $this->args_value_matches( $request_items, 'action', 'delete_transients_emanon_setting' ) ) {
1125 $is_rule_removed = true;
1126 }
1127 }
1128 }
1129 }
1130 }
1131
1132 if ( isset( $remove_rules['vkexunit'] ) ) {
1133 // vkExUnitルール除外(メイン設定)
1134 if ( ( $request_items['args_get']['page'] ?? '' ) === 'vkExUnit_main_setting' ) {
1135 if ( preg_match( '/wp-admin\/admin\.php/', $request_items['request_filename'] ) === 1 ) {
1136 if ( isset( $request_items['args_post']['_nonce_vkExUnit'] ) ) {
1137 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
1138 $is_rule_removed = true;
1139 }
1140 }
1141 }
1142 }
1143
1144 // vkExUnitルール除外(cssカスタマイズ)
1145 if ( preg_match( '/wp-admin\/admin.php\?page\=vkExUnit_css_customize/', $_SERVER['REQUEST_URI'] ?? '' ) === 1 ) {
1146 if ( in_array( $rule_id, $remove_rules['vkexunit'], true ) ) {
1147 if ( $this->args_value_matches_partial( $request_items, '_wp_http_referer', '/wp-admin/admin.php?page=vkExUnit_css_customize' ) ) {
1148 $is_rule_removed = true;
1149 }
1150 }
1151 }
1152 }
1153
1154 if ( isset( $remove_rules['nishiki'] ) ) {
1155 // nishikiルール除外
1156 if ( $this->args_value_matches_regex( $request_items, 'option_page', '/^nishiki_pro_general/' ) ) {
1157 if ( preg_match( '/wp-admin\/options\.php/', $request_items['request_filename'] ) === 1 ) {
1158 if ( in_array( $rule_id, $remove_rules['nishiki'], true ) ) {
1159 if ( $this->args_value_matches( $request_items, 'action', 'update' ) ) {
1160 $is_rule_removed = true;
1161 }
1162 }
1163 }
1164 }
1165 }
1166
1167 if ( isset( $remove_rules['swell'] ) ) {
1168 // swellルール除外
1169 if ( $this->args_value_matches_regex( $request_items, 'option_page', '/^swell_setting_group_editor/' ) ) {
1170 if ( preg_match( '/wp-admin\/options\.php/', $request_items['request_filename'] ) === 1 ) {
1171 if ( in_array( $rule_id, $remove_rules['swell'], true ) ) {
1172 if ( $this->args_value_matches( $request_items, 'action', 'update' ) ) {
1173 $is_rule_removed = true;
1174 }
1175 }
1176 }
1177 }
1178 }
1179
1180 if ( isset( $remove_rules['comment'] ) ) {
1181 // コメント編集時の除外
1182 if ( preg_match( '/wp-admin\/comment\.php/', $request_items['request_filename'] ?? '' ) === 1 ) {
1183 if ( in_array( $rule_id, $remove_rules['comment'], true ) ) {
1184 if ( $this->args_value_matches( $request_items, 'action', 'editedcomment' ) ) {
1185 $is_rule_removed = true;
1186 }
1187 }
1188 }
1189 }
1190
1191 if ( isset( $remove_rules['ajax_editor'] ) ) {
1192 // テーマ・プラグインエディターの操作時の除外
1193 if ( preg_match( '/wp-admin\/admin-ajax\.php/', $request_items['request_filename'] ) === 1 ) {
1194 if ( in_array( $rule_id, $remove_rules['ajax_editor'], true ) ) {
1195 if ( $this->args_value_matches_regex( $request_items, '_wp_http_referer', '/theme-editor(\.php)?|plugin-editor(\.php)?/' ) ) {
1196 $is_rule_removed = true;
1197 }
1198
1199 // オートセーブ時
1200 $screen_id = $request_items['args_post']['screen_id'] ?? '';
1201 if ( preg_match( '/theme-editor(\.php)?|plugin-editor(\.php)?/', $screen_id ) === 1 ) {
1202 $is_rule_removed = true;
1203 }
1204 }
1205 }
1206 }
1207
1208 if ( isset( $remove_rules['rename_login_page'] ) ) {
1209 $login_page_name = $remove_rules['rename_login_page']['login_page_name'];
1210 $is_target_rule = in_array( $rule_id, $remove_rules['rename_login_page']['rule_ids'], true );
1211
1212 if ( $is_target_rule ) {
1213 // ログインURL変更設定保存時:rename_login_page_nameキーの値のみ除外
1214 if ( preg_match( '/wp-admin\/admin\.php/', $request_items['request_filename'] ) === 1 ) {
1215 if ( ( $request_items['args_get']['page'] ?? '' ) === 'cloudsecurewp_rename_login_page' ) {
1216 $modify_remove_variables['args_post'] = array( 'rename_login_page_name' );
1217 }
1218 }
1219
1220 // 変更後ログインURLへのアクセス時、またはリファラーが変更後ログインURLである時の対応
1221 if ( ! empty( $login_page_name ) ) {
1222 $login_page_pattern = '/\/' . preg_quote( $login_page_name, '/' ) . '(?:[\/\?#]|$)/';
1223 $is_login_page_access = preg_match( $login_page_pattern, $request_items['request_filename'] ) === 1;
1224 $is_login_page_referer = preg_match( $login_page_pattern, $_SERVER['HTTP_REFERER'] ?? '' ) === 1;
1225
1226 if ( $is_login_page_access ) {
1227 // 1. URI自体に誤検知ワードが含まれるため、request_filenameを除外
1228 $modify_variables[] = self::VARIABLE_REQUEST_FILENAME;
1229
1230 // 2. args の特定キー(リダイレクト系など)を除外
1231 if ( ! isset( $modify_remove_variables['args_get'] ) ) {
1232 $modify_remove_variables['args_get'] = array();
1233 }
1234 $modify_remove_variables['args_get'] = array_merge( $modify_remove_variables['args_get'], array( 'redirect_to', '_wp_http_referer' ) );
1235
1236 if ( ! isset( $modify_remove_variables['args_post'] ) ) {
1237 $modify_remove_variables['args_post'] = array();
1238 }
1239 $modify_remove_variables['args_post'] = array_merge( $modify_remove_variables['args_post'], array( 'redirect_to', '_wp_http_referer' ) );
1240 }
1241
1242 if ( $is_login_page_access || $is_login_page_referer ) {
1243 // 3. リファラーヘッダーを除外
1244 if ( ! isset( $modify_remove_variables['request_headers'] ) ) {
1245 $modify_remove_variables['request_headers'] = array();
1246 }
1247 $modify_remove_variables['request_headers'][] = 'Referer';
1248 }
1249 }
1250 }
1251 }
1252
1253 return array(
1254 'is_removed' => $is_rule_removed,
1255 'modify_remove_variables' => $modify_remove_variables,
1256 'modify_variables' => $modify_variables,
1257 );
1258 }
1259
1260 /**
1261 * Advanced Custom Fieldsプラグイン除外対応
1262 * 有効なカスタム投稿タイプキーを取得する
1263 *
1264 * @return array
1265 */
1266 public function get_acf_post_types(): array {
1267 global $wpdb;
1268 $active_plugins = get_option( 'active_plugins' );
1269 $acf_post_types = array();
1270
1271 if ( is_array( $active_plugins ) && preg_match( '/advanced-custom-fields/', implode( ',', $active_plugins ) ) ) {
1272 $results = $wpdb->get_results(
1273 $wpdb->prepare(
1274 "SELECT post_content
1275 FROM {$wpdb->posts}
1276 WHERE post_type = %s
1277 AND post_status = %s",
1278 'acf-post-type',
1279 'publish'
1280 )
1281 );
1282
1283 if ( ! empty( $results ) ) {
1284 foreach ( $results as $result ) {
1285 $post_content = unserialize( $result->post_content, [ 'allowed_classes' => false ] );
1286
1287 if ( is_array( $post_content ) && isset( $post_content['post_type'] ) ) {
1288 $acf_post_types[] = $post_content['post_type'];
1289 }
1290 }
1291 }
1292 }
1293
1294 return $acf_post_types;
1295 }
1296
1297
1298 /**
1299 * Custom Post Type UIプラグイン除外対応
1300 * 有効なカスタム投稿タイプキーを取得する
1301 *
1302 * @return array
1303 */
1304 public function get_cptui_post_types(): array {
1305 $active_plugins = get_option( 'active_plugins' );
1306 $cptui_post_types = array();
1307
1308 if ( is_array( $active_plugins ) && preg_match( '/custom-post-type-ui/', implode( ',', $active_plugins ) ) ) {
1309 $cptui_data = get_option( 'cptui_post_types' );
1310
1311 if ( is_string( $cptui_data ) ) {
1312 $cptui_data = unserialize( $cptui_data, [ 'allowed_classes' => false ] );
1313 }
1314
1315 if ( is_array( $cptui_data ) ) {
1316 foreach ( $cptui_data as $post_type ) {
1317 if ( is_array( $post_type ) && isset( $post_type['name'] ) ) {
1318 $cptui_post_types[] = $post_type['name'];
1319 }
1320 }
1321 }
1322 }
1323
1324 return $cptui_post_types;
1325 }
1326
1327
1328 /**
1329 * バックトラック�
1330 過が発生したパラメータの表示用文字列を生成
1331 *
1332 * @param string $variable WAF 変数種別
1333 * @param string $key パラメータ名
1334 * @return string
1335 */
1336 private function format_backtrack_key( string $variable, string $key ): string {
1337 switch ( $variable ) {
1338 // REQUEST_FILENAME / XML は単一文字列を検査するためパラメータ名(キー)を持たず、固定ラベルを返す
1339 case self::VARIABLE_REQUEST_FILENAME:
1340 return 'URL';
1341
1342 case self::VARIABLE_XML:
1343 return 'XML';
1344
1345 case self::VARIABLE_ARGS_GET:
1346 case self::VARIABLE_ARGS_GET_NAMES:
1347 $label = 'GET';
1348 break;
1349
1350 case self::VARIABLE_ARGS_POST:
1351 case self::VARIABLE_ARGS_POST_NAMES:
1352 $content_type = isset( $_SERVER['CONTENT_TYPE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['CONTENT_TYPE'] ) ) : '';
1353 $label = ( false !== strpos( $content_type, 'application/json' ) ) ? 'JSON' : 'POST';
1354 break;
1355
1356 case self::VARIABLE_REQUEST_COOKIES:
1357 case self::VARIABLE_REQUEST_COOKIES_NAMES:
1358 $label = 'COOKIE';
1359 break;
1360
1361 case self::VARIABLE_REQUEST_HEADERS:
1362 $label = 'HEADER';
1363 break;
1364
1365 // 到達しない想定
1366 default:
1367 return '';
1368 }
1369
1370 // パラメータ名が取得できない場合(JSONの空文字キー等)はカテゴリ名のみを返し、
1371 // ログの検知データ欄が空文字にならないようにする
1372 if ( '' === $key ) {
1373 return $label;
1374 }
1375
1376 return $label . ': ' . $key;
1377 }
1378
1379
1380 /**
1381 * 除外対象ユーザーの判定(Cookie認証 + 編集権限)
1382 *
1383 * シンプルWAFは plugins_loaded(優�
1384 �度11)段階で実行されるが、この段階でも
1385 * wp_validate_auth_cookie() / user_can() は使用できる。Cookie認証以外
1386 * (Application Password、JWT、OAuth、Basic認証等)はこの判定では未ログイン扱いとなる。
1387 *
1388 * 除外条件は REST API無効化機能(disable-restapi.php)と揃え、
1389 * edit_pages または edit_posts を持つユーザー(=寄稿�
1390 以上)のみを対象とする。
1391 *
1392 * @return bool true: 除外対象(編集権限を持つログインユーザー) / false: それ以外
1393 */
1394 private function is_excluded_user_by_cookie(): bool {
1395 if ( null === $this->excluded_user_by_cookie ) {
1396 $this->excluded_user_by_cookie = false;
1397
1398 if ( function_exists( 'wp_validate_auth_cookie' ) && function_exists( 'user_can' ) ) {
1399 $user_id = wp_validate_auth_cookie( '', 'logged_in' );
1400 if ( $user_id ) {
1401 $this->excluded_user_by_cookie =
1402 user_can( $user_id, 'edit_pages' ) || user_can( $user_id, 'edit_posts' );
1403 }
1404 }
1405 }
1406
1407 return $this->excluded_user_by_cookie;
1408 }
1409
1410
1411 /**
1412 * バックトラック�
1413 過エラーの結果�
1414 �列を生成
1415 *
1416 * @param array $request_items
1417 * @param string $variable WAF 変数種別
1418 * @param string $key パラメータ名
1419 * @return array
1420 */
1421 private function get_backtrack_error_results( array $request_items, string $variable, string $key ): array {
1422 $matched = $this->format_backtrack_key( $variable, $key );
1423
1424 // バックトラックエラー記録用のURL生成
1425 if ( isset( $_SERVER['HTTP_HOST'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
1426 $backtrack_url = ( empty( $_SERVER['HTTPS'] ) ? 'http://' : 'https://' )
1427 . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) )
1428 . esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
1429 } else {
1430 $backtrack_url = '';
1431 }
1432
1433 return array(
1434 'matched' => $matched,
1435 'ip' => $request_items['ip'],
1436 'access_at' => $request_items['access_at'],
1437 'url' => $backtrack_url,
1438 'is_backtrack_error' => true,
1439 );
1440 }
1441
1442
1443 /**
1444 * waf_engine
1445 *
1446 * @param array $waf_rules
1447 * @param array $locationmatch_rules
1448 * @param int $available_rules
1449 * @param array $remove_rules
1450 * @return array
1451 */
1452 public function waf_engine( $waf_rules, $locationmatch_rules, $available_rules, $remove_rules, $deny_on_backtrack_error = '1' ): array {
1453 $request_items = $this->get_request_items();
1454
1455 $locationmatch_removed_rule_ids = $this->locationmatch_remove_rules( $locationmatch_rules, $_SERVER['REQUEST_URI'] ?? '' );
1456 $skip = 0;
1457 $skipafter = '';
1458 $chain_items = array();
1459 $tmp_match_results = array();
1460
1461 // Advanced Custom Fieldsプラグイン除外対応で追加
1462 $acf_post_types = $this->get_acf_post_types();
1463 // Custom Post Type UIプラグイン除外対応で追加
1464 $cptui_post_types = $this->get_cptui_post_types();
1465
1466 foreach ( $waf_rules as $waf_rule ) {
1467 // 前回マッチしたルールからskipの設定を引き継いでいる場合はスキップ
1468 if ( $this->is_skip_enabled( $skip ) ) {
1469 $skip--;
1470 continue;
1471 }
1472
1473 // 前回マッチしたルールからskipafterの設定を引き継いでいる場合は現在のルールIDと比較し、一致するまでスキップ
1474 // ルールIDの比較結果が同じの場合は、$skipafterを初期化して次のルールから判定を行う
1475 if ( $this->is_skipafter_enabled( $skipafter ) ) {
1476 if ( $skipafter !== $waf_rule['id'] ) {
1477 continue;
1478 } else {
1479 $skipafter = '';
1480 continue;
1481 }
1482 }
1483
1484 // LocationMatchによるルールの除外があるか確認。ある場合は現在のルールIDと比較し、一致する場合はスキップ
1485 if ( ! empty( $locationmatch_removed_rule_ids ) ) {
1486 if ( in_array( $waf_rule['id'], $locationmatch_removed_rule_ids, true ) ) {
1487 continue;
1488 }
1489 }
1490
1491 // ルールにvariables設定がない場合、skip,akipafterの設定を確認して次のルール判定へ
1492 if ( empty( $waf_rule['variables'] ) ) {
1493 $skip = $this->check_skip( $waf_rule['skip'] );
1494 $skipafter = $this->check_skipafter( $waf_rule['skipafter'] );
1495 continue;
1496 }
1497
1498 // 特定の操作の場合、特定のルールを除外する
1499 $remove_rule_result = $this->is_remove_rule( $waf_rule['id'], $request_items, $remove_rules, $acf_post_types, $cptui_post_types );
1500
1501 if ( $remove_rule_result['is_removed'] ) {
1502 continue;
1503 }
1504
1505 // ルールのremove_variablesを動的に変更
1506 if ( ! empty( $remove_rule_result['modify_remove_variables'] ) ) {
1507 $waf_rule['remove_variables'] = array_merge_recursive(
1508 $waf_rule['remove_variables'],
1509 $remove_rule_result['modify_remove_variables']
1510 );
1511 }
1512
1513 // ルールのvariablesを動的に変更
1514 if ( ! empty( $remove_rule_result['modify_variables'] ) ) {
1515 $waf_rule['variables'] = array_diff(
1516 $waf_rule['variables'],
1517 $remove_rule_result['modify_variables']
1518 );
1519 }
1520
1521 foreach ( $waf_rule['variables'] as $variable ) {
1522 switch ( $variable ) {
1523 case self::VARIABLE_ARGS_GET:
1524 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_GET, $chain_items );
1525 break;
1526
1527 case self::VARIABLE_ARGS_GET_NAMES:
1528 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_GET_NAMES, $chain_items );
1529 break;
1530
1531 case self::VARIABLE_ARGS_POST:
1532 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_POST, $chain_items );
1533 break;
1534
1535 case self::VARIABLE_ARGS_POST_NAMES:
1536 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_ARGS_POST_NAMES, $chain_items );
1537 break;
1538
1539 case self::VARIABLE_REQUEST_COOKIES:
1540 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES, $chain_items );
1541 break;
1542
1543 case self::VARIABLE_REQUEST_COOKIES_NAMES:
1544 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_COOKIES_NAMES, $chain_items );
1545 break;
1546
1547 case self::VARIABLE_REQUEST_HEADERS:
1548 $results = $this->check_request_item_array( $waf_rule, $request_items, self::VARIABLE_REQUEST_HEADERS, $chain_items );
1549 break;
1550
1551 case self::VARIABLE_REQUEST_FILENAME:
1552 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_REQUEST_FILENAME, $chain_items );
1553 break;
1554
1555 case self::VARIABLE_XML:
1556 $results = $this->check_request_item_strings( $waf_rule, $request_items, self::VARIABLE_XML, $chain_items );
1557 break;
1558 }
1559
1560 // プリフィルタ(attack='')でバックトラック�
1561 過エラーが発生した場合は一致側に倒し、
1562 // 除外対象に関わらず、後続の精密ルールに進んで判定する
1563 if ( ! empty( $results['has_backtrack_error'] ) && '' === $waf_rule['attack'] ) {
1564 $results = $this->get_rule_settings_and_results( $waf_rule, $request_items, $variable, $chain_items, '' );
1565 }
1566
1567 // バックトラック�
1568 過エラーの処理
1569 // 編集権限を持つログインユーザー(Cookie認証)は遮断・ログ記録の対象外とし、
1570 // それ以外(未ログイン・権限なしユーザー)の場合のみ以降の判定を行う
1571 if ( ! empty( $results['has_backtrack_error'] ) && ! $this->is_excluded_user_by_cookie() ) {
1572 $backtrack_error_result = $this->get_backtrack_error_results( $request_items, $variable, $results['backtrack_key'] ?? '' );
1573
1574 // 有効な攻撃種別かつ即遮断設定('1')の場合のみ遮断する。
1575 if ( ( $waf_rule['attack'] & $available_rules ) !== 0 && $deny_on_backtrack_error === '1' ) {
1576 // バックトラック�
1577 過発生時点で即遮断
1578 $backtrack_error_result['is_deny'] = true;
1579 $backtrack_error_result['is_write_log'] = true;
1580
1581 return $backtrack_error_result;
1582 }
1583 // 遮断しない場合(無効化された攻撃種別、または '0':ログのみ設定)は
1584 // バックトラックエラーをログ候補として保持し、次のルールへ続行
1585 $tmp_match_results = $backtrack_error_result;
1586 }
1587
1588 $is_matched = $results['is_matched'];
1589
1590 if ( $is_matched ) {
1591 $skip = $results['skip'];
1592 $skipafter = $results['skipafter'];
1593 $chain_items = $results['chain_items'];
1594 $match_results = $results['match_results'];
1595
1596 // マッチしたが、chain,skip,skipafterの設定がある場合は次のルール判定へ
1597 if ( ! empty( $chain_items ) || 0 < $skip || ! empty( $skipafter ) ) {
1598 break;
1599 }
1600
1601 // マッチしたが、除外設定されているルールの場合は値を保持して次のルール判定へ
1602 if ( ( $waf_rule['attack'] & $available_rules ) === 0 ) {
1603 $tmp_match_results = $match_results;
1604 break;
1605 }
1606
1607 // マッチした結果がある場合は終了処理へ(ログ記述、通知、画面表示)
1608 if ( ! empty( $match_results ) ) {
1609 $match_results['is_deny'] = true;
1610 $match_results['is_write_log'] = true;
1611 $match_results['is_backtrack_error'] = false;
1612
1613 return $match_results;
1614 }
1615 }
1616
1617 $chain_items = array();
1618 }
1619 }
1620
1621 if ( ! empty( $tmp_match_results ) ) {
1622 $match_results = $tmp_match_results;
1623 $match_results['is_deny'] = false;
1624 $match_results['is_write_log'] = true;
1625 $match_results['is_backtrack_error'] = ! empty( $match_results['is_backtrack_error'] );
1626
1627 } else {
1628 $match_results['is_deny'] = false;
1629 $match_results['is_write_log'] = false;
1630 $match_results['is_backtrack_error'] = false;
1631 }
1632
1633 return $match_results;
1634 }
1635 }
1636