← All changes
|
jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php
+150
-119
13.5.2
→
16.3
View file →
| @@ -19,9 +19,9 @@ | ||
| 19 | 19 | * @var string JETPACK_WAF_MODE |
| 20 | 20 | */ |
| 21 | 21 | |
| 22 | 22 | // Type aliases for this file. |
| 23 | -<<<PHAN | |
| 23 | +<<<'PHAN' | |
| 24 | 24 | @phan-type Target = array{ only?: string[], except?: string[], count?: boolean } |
| 25 | 25 | @phan-type TargetBag = array<string, Target> |
| 26 | 26 | PHAN; |
| 27 | 27 | |
| @@ -38,8 +38,16 @@ | ||
| 38 | 38 | */ |
| 39 | 39 | const NORMALIZE_ARRAY_MATCH_VALUES = 2; |
| 40 | 40 | |
| 41 | 41 | /** |
| 42 | + * The version of this runtime class. Used by rule files to ensure compatibility. | |
| 43 | + * | |
| 44 | + * @since 0.21.0 | |
| 45 | + * | |
| 46 | + * @var int | |
| 47 | + */ | |
| 48 | + public $version = 1; | |
| 49 | + /** | |
| 42 | 50 | * Last rule. |
| 43 | 51 | * |
| 44 | 52 | * @var string |
| 45 | 53 | */ |
| @@ -60,9 +68,9 @@ | ||
| 60 | 68 | * Matched var names. |
| 61 | 69 | * |
| 62 | 70 | * @var array |
| 63 | 71 | */ |
| 64 | - public $matched_var_names = array(); | |
| 72 | + public $matched_vars_names = array(); | |
| 65 | 73 | /** |
| 66 | 74 | * Matched var name. |
| 67 | 75 | * |
| 68 | 76 | * @var string |
| @@ -67,8 +75,14 @@ | ||
| 67 | 75 | * |
| 68 | 76 | * @var string |
| 69 | 77 | */ |
| 70 | 78 | public $matched_var_name = ''; |
| 79 | + /** | |
| 80 | + * Body Processor. | |
| 81 | + * | |
| 82 | + * @var string 'URLENCODED' | 'JSON' | '' | |
| 83 | + */ | |
| 84 | + private $body_processor = ''; | |
| 71 | 85 | |
| 72 | 86 | /** |
| 73 | 87 | * State. |
| 74 | 88 | * |
| @@ -188,9 +202,9 @@ | ||
| 188 | 202 | unset( $targets[ $name ] ); |
| 189 | 203 | } else { |
| 190 | 204 | // otherwise just mark single props to ignore. |
| 191 | 205 | $targets[ $name ]['except'] = array_merge( |
| 192 | - isset( $targets[ $name ]['except'] ) ? $targets[ $name ]['except'] : array(), | |
| 206 | + $targets[ $name ]['except'] ?? array(), | |
| 193 | 207 | $props |
| 194 | 208 | ); |
| 195 | 209 | } |
| 196 | 210 | } |
| @@ -209,13 +223,9 @@ | ||
| 209 | 223 | * @param bool $capture Capture. |
| 210 | 224 | * @return bool |
| 211 | 225 | */ |
| 212 | 226 | public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) { |
| 213 | - $this->matched_vars = array(); | |
| 214 | - $this->matched_var_names = array(); | |
| 215 | - $this->matched_var = ''; | |
| 216 | - $this->matched_var_name = ''; | |
| 217 | - $match_found = false; | |
| 227 | + $match_found = false; | |
| 218 | 228 | |
| 219 | 229 | // get values. |
| 220 | 230 | $values = $this->normalize_targets( $targets ); |
| 221 | 231 | |
| @@ -236,14 +246,14 @@ | ||
| 236 | 246 | // If either: |
| 237 | 247 | // - rule is negated ("not" flag set) and the target was not matched |
| 238 | 248 | // - rule not negated and the target was matched |
| 239 | 249 | // then this is considered a match. |
| 240 | - $match_found = true; | |
| 241 | - $this->matched_var_names[] = $v['source']; | |
| 242 | - $this->matched_vars[] = $v['value']; | |
| 243 | - $this->matched_var_name = end( $this->matched_var_names ); | |
| 244 | - $this->matched_var = end( $this->matched_vars ); | |
| 245 | - $matched[] = array( $v, $match ); | |
| 250 | + $match_found = true; | |
| 251 | + $this->matched_vars_names[] = $v['name']; | |
| 252 | + $this->matched_vars[] = $v['value']; | |
| 253 | + $this->matched_var_name = end( $this->matched_vars_names ); | |
| 254 | + $this->matched_var = end( $this->matched_vars ); | |
| 255 | + $matched[] = array( $v, $match ); | |
| 246 | 256 | // Set any captured matches into state if the rule has the "capture" flag. |
| 247 | 257 | if ( $capture ) { |
| 248 | 258 | $captures = is_array( $match ) ? $match : array( $match ); |
| 249 | 259 | foreach ( array_slice( $captures, 0, 10 ) as $i => $c ) { |
| @@ -256,130 +266,85 @@ | ||
| 256 | 266 | return $match_found; |
| 257 | 267 | } |
| 258 | 268 | |
| 259 | 269 | /** |
| 260 | - * Block. | |
| 270 | + * Generate a secure hash for an IP address. | |
| 261 | 271 | * |
| 262 | - * @param string $action Action. | |
| 263 | - * @param string $rule_id Rule id. | |
| 264 | - * @param string $reason Block reason. | |
| 265 | - * @param int $status_code Http status code. | |
| 272 | + * @param string $ip IP address. | |
| 273 | + * @return string Hashed IP. | |
| 266 | 274 | */ |
| 267 | - public function block( $action, $rule_id, $reason, $status_code = 403 ) { | |
| 268 | - if ( ! $reason ) { | |
| 269 | - $reason = "rule $rule_id"; | |
| 270 | - } else { | |
| 271 | - $reason = $this->sanitize_output( $reason ); | |
| 272 | - } | |
| 273 | - | |
| 274 | - $this->write_blocklog( $rule_id, $reason ); | |
| 275 | - error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" ); | |
| 276 | - header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" ); | |
| 277 | - if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) { | |
| 278 | - $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP'; | |
| 279 | - header( $protocol . ' 403 Forbidden', true, $status_code ); | |
| 280 | - die( "rule $rule_id - reason $reason" ); | |
| 281 | - } | |
| 275 | + private function get_ip_hash( string $ip ): string { | |
| 276 | + $hash_key = wp_salt( 'auth' ); | |
| 277 | + return hash_hmac( 'sha256', $ip, $hash_key ); | |
| 282 | 278 | } |
| 283 | 279 | |
| 284 | 280 | /** |
| 285 | - * Get the headers for logging purposes. | |
| 281 | + * Check if the IP is allowed for recovery. | |
| 282 | + * | |
| 283 | + * @param string $ip IP address. | |
| 284 | + * @return bool | |
| 286 | 285 | */ |
| 287 | - public function get_request_headers() { | |
| 288 | - $all_headers = getallheaders(); | |
| 289 | - $exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' ); | |
| 290 | - | |
| 291 | - foreach ( $exclude_headers as $header ) { | |
| 292 | - unset( $all_headers[ $header ] ); | |
| 293 | - } | |
| 294 | - | |
| 295 | - return $all_headers; | |
| 286 | + public function is_ip_allowed_for_recovery( string $ip ): bool { | |
| 287 | + $allow_hash = get_transient( 'jetpack_waf_recovery_' . $ip ); | |
| 288 | + return $allow_hash && hash_equals( $allow_hash, $this->get_ip_hash( $ip ) ); | |
| 296 | 289 | } |
| 297 | 290 | |
| 298 | 291 | /** |
| 299 | - * Write block logs. We won't write to the file if it exceeds 100 mb. | |
| 292 | + * Process a recovery attempt. | |
| 300 | 293 | * |
| 301 | - * @param string $rule_id Rule id. | |
| 302 | - * @param string $reason Block reason. | |
| 294 | + * @param string $real_ip The real IP address of the request. | |
| 303 | 295 | */ |
| 304 | - public function write_blocklog( $rule_id, $reason ) { | |
| 305 | - $log_data = array(); | |
| 306 | - $log_data['rule_id'] = $rule_id; | |
| 307 | - $log_data['reason'] = $reason; | |
| 308 | - $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' ); | |
| 309 | - $log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash | |
| 310 | - $log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash | |
| 311 | - $log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash | |
| 312 | - $log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash | |
| 313 | - $log_data['get_params'] = json_encode( $_GET ); | |
| 296 | + private function allow_login_or_prompt_recovery( $real_ip ) { | |
| 297 | + $blocked_login_page = Waf_Blocked_Login_Page::instance( $real_ip ); | |
| 314 | 298 | |
| 315 | - if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) { | |
| 316 | - $log_data['post_params'] = json_encode( $_POST ); | |
| 317 | - $log_data['headers'] = $this->get_request_headers(); | |
| 299 | + if ( $blocked_login_page->is_blocked_user_valid() ) { | |
| 300 | + // Allow the IP to bypass the block for 15 minutes. | |
| 301 | + set_transient( 'jetpack_waf_recovery_' . $real_ip, $this->get_ip_hash( $real_ip ), 15 * 60 ); | |
| 302 | + return; | |
| 318 | 303 | } |
| 319 | 304 | |
| 320 | - if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) { | |
| 321 | - $file_path = JETPACK_WAF_DIR . '/waf-blocklog'; | |
| 322 | - $file_exists = file_exists( $file_path ); | |
| 323 | - | |
| 324 | - if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) { | |
| 325 | - $fp = fopen( $file_path, 'a+' ); | |
| 326 | - | |
| 327 | - if ( $fp ) { | |
| 328 | - try { | |
| 329 | - fwrite( $fp, json_encode( $log_data ) . "\n" ); | |
| 330 | - } finally { | |
| 331 | - fclose( $fp ); | |
| 332 | - } | |
| 333 | - } | |
| 334 | - } | |
| 335 | - } | |
| 336 | - | |
| 337 | - $this->write_blocklog_row( $log_data ); | |
| 305 | + $blocked_login_page->render_and_die(); | |
| 338 | 306 | } |
| 339 | 307 | |
| 340 | 308 | /** |
| 341 | - * Write block logs to database. | |
| 309 | + * Block. | |
| 342 | 310 | * |
| 343 | - * @param array $log_data Log data. | |
| 311 | + * @param string $action Action. | |
| 312 | + * @param string $rule_id Rule id. | |
| 313 | + * @param string $reason Block reason. | |
| 314 | + * @param int $status_code Http status code. | |
| 344 | 315 | */ |
| 345 | - private function write_blocklog_row( $log_data ) { | |
| 346 | - $conn = $this->connect_to_wordpress_db(); | |
| 316 | + public function block( $action, $rule_id, $reason, $status_code = 403 ) { | |
| 317 | + // The recovery flow needs transients, `wp_salt()` and `$pagenow`, so it cannot run in | |
| 318 | + // standalone mode, where the WAF executes from `auto_prepend_file` before WordPress. | |
| 319 | + if ( 'ip block list' === $reason && defined( 'ABSPATH' ) ) { | |
| 320 | + $real_ip = $this->request->get_real_user_ip_address(); | |
| 347 | 321 | |
| 348 | - if ( ! $conn ) { | |
| 349 | - return; | |
| 350 | - } | |
| 322 | + if ( $this->is_ip_allowed_for_recovery( $real_ip ) ) { | |
| 323 | + return; | |
| 324 | + } | |
| 351 | 325 | |
| 352 | - global $table_prefix; | |
| 353 | - | |
| 354 | - $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" ); | |
| 355 | - if ( false !== $statement ) { | |
| 356 | - $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] ); | |
| 357 | - $statement->execute(); | |
| 358 | - | |
| 359 | - if ( $conn->insert_id > 100 ) { | |
| 360 | - $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" ); | |
| 326 | + global $pagenow; | |
| 327 | + if ( isset( $pagenow ) && 'wp-login.php' === $pagenow ) { | |
| 328 | + $this->allow_login_or_prompt_recovery( $real_ip ); | |
| 329 | + return; | |
| 361 | 330 | } |
| 362 | 331 | } |
| 363 | - } | |
| 364 | 332 | |
| 365 | - /** | |
| 366 | - * Connect to WordPress database. | |
| 367 | - */ | |
| 368 | - private function connect_to_wordpress_db() { | |
| 369 | - if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) { | |
| 370 | - return; | |
| 333 | + if ( ! $reason ) { | |
| 334 | + $reason = "rule $rule_id"; | |
| 335 | + } else { | |
| 336 | + $reason = $this->sanitize_output( $reason ); | |
| 371 | 337 | } |
| 372 | 338 | |
| 373 | - require_once JETPACK_WAF_WPCONFIG; | |
| 374 | - $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli | |
| 375 | - | |
| 376 | - if ( $conn->connect_error ) { | |
| 377 | - error_log( 'Could not connect to the database:' . $conn->connect_error ); | |
| 378 | - return null; | |
| 339 | + Waf_Blocklog_Manager::write_blocklog( $rule_id, $reason ); | |
| 340 | + error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" ); | |
| 341 | + header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" ); | |
| 342 | + if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) { | |
| 343 | + $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP'; | |
| 344 | + header( $protocol . ' 403 Forbidden', true, $status_code ); | |
| 345 | + die( "rule $rule_id - reason $reason" ); | |
| 379 | 346 | } |
| 380 | - | |
| 381 | - return $conn; | |
| 382 | 347 | } |
| 383 | 348 | |
| 384 | 349 | /** |
| 385 | 350 | * Redirect. |
| @@ -390,9 +355,9 @@ | ||
| 390 | 355 | */ |
| 391 | 356 | public function redirect( $rule_id, $url ) { |
| 392 | 357 | error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" ); |
| 393 | 358 | header( "Location: $url" ); |
| 394 | - exit; | |
| 359 | + exit( 0 ); | |
| 395 | 360 | } |
| 396 | 361 | |
| 397 | 362 | /** |
| 398 | 363 | * Flag rule for removal. |
| @@ -433,11 +398,9 @@ | ||
| 433 | 398 | * |
| 434 | 399 | * @param string $key Key. |
| 435 | 400 | */ |
| 436 | 401 | public function get_var( $key ) { |
| 437 | - return isset( $this->state[ $key ] ) | |
| 438 | - ? $this->state[ $key ] | |
| 439 | - : ''; | |
| 402 | + return $this->state[ $key ] ?? ''; | |
| 440 | 403 | } |
| 441 | 404 | |
| 442 | 405 | /** |
| 443 | 406 | * Set variable value. |
| @@ -537,9 +500,9 @@ | ||
| 537 | 500 | case 'args_get_names': |
| 538 | 501 | $value = $this->args_names( $this->meta( 'args_get' ) ); |
| 539 | 502 | break; |
| 540 | 503 | case 'args_post': |
| 541 | - $value = $this->request->get_post_vars(); | |
| 504 | + $value = $this->request->get_post_vars( $this->get_body_processor() ); | |
| 542 | 505 | break; |
| 543 | 506 | case 'args_post_names': |
| 544 | 507 | $value = $this->args_names( $this->meta( 'args_post' ) ); |
| 545 | 508 | break; |
| @@ -563,8 +526,20 @@ | ||
| 563 | 526 | break; |
| 564 | 527 | case 'files_names': |
| 565 | 528 | $value = $this->args_names( $this->meta( 'files' ) ); |
| 566 | 529 | break; |
| 530 | + case 'matched_vars': | |
| 531 | + $value = array_combine( $this->matched_vars_names, $this->matched_vars ); | |
| 532 | + break; | |
| 533 | + case 'matched_var': | |
| 534 | + $value = array( $this->matched_var_name => $this->matched_var ); | |
| 535 | + break; | |
| 536 | + case 'matched_vars_names': | |
| 537 | + $value = $this->matched_vars_names; | |
| 538 | + break; | |
| 539 | + case 'matched_var_name': | |
| 540 | + $value = array( $this->matched_var_name ); | |
| 541 | + break; | |
| 567 | 542 | } |
| 568 | 543 | $this->metadata[ $key ] = $value; |
| 569 | 544 | } |
| 570 | 545 | |
| @@ -588,8 +563,30 @@ | ||
| 588 | 563 | return $output; |
| 589 | 564 | } |
| 590 | 565 | |
| 591 | 566 | /** |
| 567 | + * Get the body processor. | |
| 568 | + * | |
| 569 | + * @return string | |
| 570 | + */ | |
| 571 | + private function get_body_processor() { | |
| 572 | + return $this->body_processor; | |
| 573 | + } | |
| 574 | + | |
| 575 | + /** | |
| 576 | + * Set the body processor. | |
| 577 | + * | |
| 578 | + * @param string $processor Processor to set. Either 'URLENCODED' or 'JSON'. | |
| 579 | + * | |
| 580 | + * @return void | |
| 581 | + */ | |
| 582 | + public function set_body_processor( $processor ) { | |
| 583 | + if ( $processor === 'URLENCODED' || $processor === 'JSON' ) { | |
| 584 | + $this->body_processor = $processor; | |
| 585 | + } | |
| 586 | + } | |
| 587 | + | |
| 588 | + /** | |
| 592 | 589 | * Change a string to all lowercase and replace spaces and underscores with dashes. |
| 593 | 590 | * |
| 594 | 591 | * @param string $name Name. |
| 595 | 592 | * @return string |
| @@ -622,10 +619,10 @@ | ||
| 622 | 619 | public function normalize_targets( $targets ) { |
| 623 | 620 | $return = array(); |
| 624 | 621 | foreach ( $targets as $k => $v ) { |
| 625 | 622 | $count_only = isset( $v['count'] ) ? self::NORMALIZE_ARRAY_COUNT : 0; |
| 626 | - $only = isset( $v['only'] ) ? $v['only'] : array(); | |
| 627 | - $except = isset( $v['except'] ) ? $v['except'] : array(); | |
| 623 | + $only = $v['only'] ?? array(); | |
| 624 | + $except = $v['except'] ?? array(); | |
| 628 | 625 | $_k = strtolower( $k ); |
| 629 | 626 | switch ( $_k ) { |
| 630 | 627 | case 'request_headers': |
| 631 | 628 | $this->normalize_array_target( |
| @@ -677,11 +674,27 @@ | ||
| 677 | 674 | $this->meta( substr( $_k, 0, -6 ) ) |
| 678 | 675 | ); |
| 679 | 676 | $this->normalize_array_target( $data, $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES ); |
| 680 | 677 | continue 2; |
| 678 | + case 'matched_var': | |
| 679 | + $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only ); | |
| 680 | + continue 2; | |
| 681 | + | |
| 682 | + case 'matched_var_name': | |
| 683 | + $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES ); | |
| 684 | + continue 2; | |
| 685 | + | |
| 686 | + case 'matched_vars': | |
| 687 | + $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only ); | |
| 688 | + continue 2; | |
| 689 | + | |
| 690 | + case 'matched_vars_names': | |
| 691 | + $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES ); | |
| 692 | + continue 2; | |
| 693 | + | |
| 681 | 694 | default: |
| 682 | 695 | var_dump( 'Unknown target', $k, $v ); |
| 683 | - exit; | |
| 696 | + exit( 0 ); | |
| 684 | 697 | } |
| 685 | 698 | $return[] = array( |
| 686 | 699 | 'name' => $k, |
| 687 | 700 | 'value' => $v, |
| @@ -692,8 +705,26 @@ | ||
| 692 | 705 | return $return; |
| 693 | 706 | } |
| 694 | 707 | |
| 695 | 708 | /** |
| 709 | + * Reset matched vars after processing a rule. | |
| 710 | + * | |
| 711 | + * @return void | |
| 712 | + */ | |
| 713 | + public function reset_matched_vars() { | |
| 714 | + $this->matched_vars = array(); | |
| 715 | + $this->matched_vars_names = array(); | |
| 716 | + $this->matched_var = ''; | |
| 717 | + $this->matched_var_name = ''; | |
| 718 | + unset( | |
| 719 | + $this->metadata['matched_var'], | |
| 720 | + $this->metadata['matched_vars'], | |
| 721 | + $this->metadata['matched_vars_names'], | |
| 722 | + $this->metadata['matched_var_name'] | |
| 723 | + ); | |
| 724 | + } | |
| 725 | + | |
| 726 | + /** | |
| 696 | 727 | * Verifies if the IP from the current request is in an array. |
| 697 | 728 | * |
| 698 | 729 | * @param array $array Array of IP addresses to verify the request IP against. |
| 699 | 730 | * @return bool |
| @@ -702,10 +733,10 @@ | ||
| 702 | 733 | $real_ip = $this->request->get_real_user_ip_address(); |
| 703 | 734 | $array_length = count( $array ); |
| 704 | 735 | |
| 705 | 736 | for ( $i = 0; $i < $array_length; $i++ ) { |
| 706 | - // Check if the IP matches a provided range. | |
| 707 | - $range = explode( '-', $array[ $i ] ); | |
| 737 | + // Check if the IP matches a provided range or CIDR notation. | |
| 738 | + $range = strpos( $array[ $i ], '/' ) !== false ? array( $array[ $i ], null ) : explode( '-', $array[ $i ] ); | |
| 708 | 739 | if ( count( $range ) === 2 ) { |
| 709 | 740 | if ( IP_Utils::ip_address_is_in_range( $real_ip, $range[0], $range[1] ) ) { |
| 710 | 741 | return true; |
| 711 | 742 | } |