File: /var/www/html/wpmuhibbah_err/wp-content/plugins/defender-security/src/traits/ip.php
<?php
/**
* Helper functions for IP related tasks.
*
* @package WP_Defender\Traits
*/
namespace WP_Defender\Traits;
use WP_Defender\Component\Http\Remote_Address;
use WP_Defender\Component\Smart_Ip_Detection;
use WP_Defender\Model\Setting\Firewall;
trait IP {
/**
* Check if the IP is IPv4 address.
*
* @param mixed $ip IP address.
*
* @return mixed
*/
private function is_v4( $ip ) {
return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 );
}
/**
* Check if the given IP address is an IPv6 address.
*
* @param string $ip The IP address to check.
*
* @return bool Returns true if the IP address is an IPv6 address, false otherwise.
*/
private function is_v6( $ip ) {
return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );
}
/**
* Check if IPv6 is supported.
*
* @return bool
*/
private function is_v6_support(): bool {
return defined( 'AF_INET6' );
}
/**
* Convert IPv4 address to IPv6 address.
*
* @param string $ip IPv4 address.
*
* @return bool|string
*/
private function expand_ip_v6( $ip ) {
$hex = unpack( 'H*hex', inet_pton( $ip ) );
return substr( preg_replace( '/([A-f0-9]{4})/', '$1:', $hex['hex'] ), 0, - 1 );
}
/**
* Convert IPv6 address to binary.
*
* @param string $inet The packed data.
* @src https://stackoverflow.com/a/7951507
*
* @return string
*/
private function ine_to_bits( $inet ): string {
$unpacked = unpack( 'a16', $inet );
$unpacked = str_split( $unpacked[1] );
$binaryip = '';
foreach ( $unpacked as $char ) {
$binaryip .= str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
}
return $binaryip;
}
/**
* Compare if an IPv4 address is within a specified range.
*
* @param string $ip The IPv4 address to compare.
* @param string $first_in_range The lower bound of the range.
* @param string $last_in_range The upper bound of the range.
*
* @return bool Returns true if the IP address is within the range, false otherwise.
*/
private function compare_v4_in_range( $ip, $first_in_range, $last_in_range ): bool {
$low = sprintf( '%u', ip2long( $first_in_range ) );
$high = sprintf( '%u', ip2long( $last_in_range ) );
$cip = sprintf( '%u', ip2long( $ip ) );
if ( $high >= $cip && $cip >= $low ) {
return true;
}
return false;
}
/**
* Compare if an IPv6 address is within a specified range.
*
* @param string $ip The IPv6 address to compare.
* @param string $first_in_range The lower bound of the range.
* @param string $last_in_range The upper bound of the range.
*
* @return bool Returns true if the IP address is within the range, false otherwise.
*/
private function compare_v6_in_range( $ip, $first_in_range, $last_in_range ): bool {
$first_in_range = inet_pton( $this->expand_ip_v6( $first_in_range ) );
$last_in_range = inet_pton( $this->expand_ip_v6( $last_in_range ) );
$ip = inet_pton( $this->expand_ip_v6( $ip ) );
if ( ( strlen( $ip ) === strlen( $first_in_range ) )
&& ( $ip >= $first_in_range && $ip <= $last_in_range ) ) {
return true;
} else {
return false;
}
}
/**
* Compares an IPv4 address with a CIDR block.
*
* @param string $ip The IPv4 address to compare.
* @param string $block The CIDR block to compare against.
*
* @return bool Returns true if the IP address is within the CIDR block, false otherwise.
*/
private function compare_cidrv4( $ip, $block ): bool {
[ $subnet, $bits ] = explode( '/', $block );
if ( is_null( $bits ) ) {
$bits = 32;
}
$ip = ip2long( $ip );
$subnet = ip2long( $subnet );
$mask = - 1 << ( 32 - $bits );
$subnet &= $mask;// nb: in case the supplied subnet wasn't correctly aligned.
return ( $ip & $mask ) === $subnet;
}
/**
* Compares an IPv6 address with a CIDR block.
*
* @param string $ip The IPv6 address to compare.
* @param string $block The CIDR block to compare against.
*
* @return bool Returns true if the IP address is within the CIDR block, false otherwise.
*/
private function compare_cidrv6( $ip, $block ): bool {
$ip = $this->expand_ip_v6( $ip );
$ip = inet_pton( $ip );
$b_ip = $this->ine_to_bits( $ip );
[ $subnet, $bits ] = explode( '/', $block );
$subnet = $this->expand_ip_v6( $subnet );
$subnet = inet_pton( $subnet );
$b_subnet = $this->ine_to_bits( $subnet );
$ip_net_bits = substr( $b_ip, 0, $bits );
$subnet_bits = substr( $b_subnet, 0, $bits );
return $ip_net_bits === $subnet_bits;
}
/**
* Compare ip2 to ip1, true if ip2>ip1, false if not.
*
* @param string $ip1 The first IP address to compare.
* @param string $ip2 The second IP address to compare.
*
* @return bool Returns true if ip2 is greater than ip1, false otherwise.
*/
public function compare_ip( $ip1, $ip2 ): bool {
if ( $this->is_v4( $ip1 ) && $this->is_v4( $ip2 ) ) {
if ( sprintf( '%u', ip2long( $ip2 ) ) - sprintf( '%u', ip2long( $ip1 ) ) > 0 ) {
return true;
}
} elseif ( $this->is_v6( $ip1 ) && $this->is_v6( $ip2 ) && $this->is_v6_support() ) {
$ip1 = inet_pton( $this->expand_ip_v6( $ip1 ) );
$ip2 = inet_pton( $this->expand_ip_v6( $ip2 ) );
return $ip2 > $ip1;
}
return false;
}
/**
* Compare if an IP address is within a specified range.
*
* @param string $ip The IP address to compare.
* @param string $first_in_range The lower bound of the range.
* @param string $last_in_range The upper bound of the range.
*
* @return bool Returns true if the IP address is within the range, false otherwise.
*/
public function compare_in_range( $ip, $first_in_range, $last_in_range ): bool {
if ( $this->is_v4( $first_in_range ) && $this->is_v4( $last_in_range ) ) {
return $this->compare_v4_in_range( $ip, $first_in_range, $last_in_range );
} elseif ( $this->is_v6( $first_in_range ) && $this->is_v6( $last_in_range ) && $this->is_v6_support() ) {
return $this->compare_v6_in_range( $ip, $first_in_range, $last_in_range );
}
return false;
}
/**
* Compares an IP address with a CIDR block.
*
* @param string $ip The IP address to compare.
* @param string $block The CIDR block to compare against.
*
* @return bool Returns true if the IP address is within the CIDR block, false otherwise.
*/
public function compare_cidr( $ip, $block ): bool {
[ $subnet, $bits ] = explode( '/', $block );
if ( $this->is_v4( $ip ) && $this->is_v4( $subnet ) ) {
return $this->compare_cidrv4( $ip, $block );
} elseif ( $this->is_v6( $ip ) && $this->is_v6( $subnet ) && $this->is_v6_support() ) {
return $this->compare_cidrv6( $ip, $block );
}
return false;
}
/**
* $ip an be single ip, or a range like xxx.xxx.xxx.xxx - xxx.xxx.xxx.xxx or CIDR.
*
* @param string $ip The IP address to validate.
*
* @return bool
*/
public function validate_ip( $ip ): bool {
if (
! stristr( $ip, '-' )
&& ! stristr( $ip, '/' )
&& filter_var( $ip, FILTER_VALIDATE_IP ) ) {
// Only ip, no '-', '/' symbols.
return true;
} elseif ( stristr( $ip, '-' ) ) {
$ips = explode( '-', $ip );
foreach ( $ips as $ip_key ) {
if ( ! filter_var( $ip_key, FILTER_VALIDATE_IP ) ) {
return false;
}
}
if ( $this->compare_ip( $ips[0], $ips[1] ) ) {
return true;
}
} elseif ( stristr( $ip, '/' ) ) {
[ $ip, $bits ] = explode( '/', $ip );
if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
if ( $this->is_v4( $ip ) && 0 <= $bits && $bits <= 32 ) {
return true;
} elseif ( $this->is_v6( $ip ) && 0 <= $bits && $bits <= 128 && $this->is_v6_support() ) {
return true;
}
}
}
return false;
}
/**
* Display a message if IP is non-valid. The cases:
* 1) single IP (no '-', '/' symbols),
* 2) IP range,
* 3) CIDR.
* Ignore cases with private, reserved ranges.
*
* @src https://en.wikipedia.org/wiki/IPv4#Special-use_addresses
* @src https://en.wikipedia.org/wiki/IPv6#Special-use_addresses
*
* @param mixed $ip IP address.
*
* @return array
*/
public function display_validation_message( $ip ): array {
$errors = array();
// Case 1: single IP.
if (
! stristr( $ip, '-' )
&& ! stristr( $ip, '/' )
&& ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
$errors[] = sprintf(
/* translators: %s: IP value. */
esc_html__( '%s – invalid format', 'defender-security' ),
'<b>' . $ip . '</b>'
);
// Case 2: IP range.
} elseif ( stristr( $ip, '-' ) ) {
$ips = explode( '-', $ip );
foreach ( $ips as $ip_key ) {
if ( ! filter_var( $ip_key, FILTER_VALIDATE_IP ) ) {
$errors[] = sprintf(
/* translators: %s: IP value. */
esc_html__( '%s – invalid format', 'defender-security' ),
'<b>' . $ip_key . '</b>'
);
}
}
if ( ! $this->compare_ip( $ips[0], $ips[1] ) ) {
$errors[] = sprintf(
/* translators: 1. IP value. 2. IP value. */
esc_html__( 'Can\'t compare %1$s with %2$s.', 'defender-security' ),
'<b>' . $ips[1] . '</b>',
'<b>' . $ips[0] . '</b>'
);
}
// Case 3: CIDR.
} elseif ( stristr( $ip, '/' ) ) {
[ $ip, $bits ] = explode( '/', $ip );
if ( filter_var( $ip, FILTER_VALIDATE_IP ) && filter_var( $bits, FILTER_VALIDATE_INT ) ) {
if ( ! $this->is_v4( $ip ) || 0 > $bits || $bits > 32 ) {
if ( ! $this->is_v6( $ip ) || 0 > $bits || $bits > 128 || ! $this->is_v6_support() ) {
$errors[] = sprintf(
/* translators: %s: IP value. */
esc_html__( '%s – address out of range', 'defender-security' ),
'<b>' . $ip . '</b>'
);
}
}
} else {
$errors[] = sprintf(
/* translators: %s: IP value. */
esc_html__( '%s – invalid format', 'defender-security' ),
'<b>' . $ip . '</b>'
);
}
}
// @since 2.6.3
return (array) apply_filters( 'wd_display_ip_validations', $errors );
}
/**
* Validate IP.
*
* @param mixed $ip IP address.
*
* @return bool
*/
public function check_validate_ip( $ip ): bool {
// Validate the localhost IP address.
if ( in_array( $ip, $this->get_localhost_ips(), true ) ) {
return true;
}
$filter_flags = FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6;
// @since 2.4.7
if ( apply_filters( 'wp_defender_filtered_internal_ip', false ) ) {
// Todo: improve display of IP log when filtering reserved or private IPv4/IPv6 ranges.
$filter_flags = $filter_flags | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
}
if ( false === filter_var( $ip, FILTER_VALIDATE_IP, $filter_flags ) ) {
return false;
}
return true;
}
/**
* Get user IP.
*
* @return array
*/
public function get_user_ip(): array {
$service = wd_di()->get( Smart_Ip_Detection::class );
if ( $service->is_smart_ip_detection_enabled() ) {
$ip_detail = $service->get_smart_ip_detection_details();
$ips = isset( $ip_detail[0] ) ? array( $ip_detail[0] ) : array();
} else {
$remote_addr = wd_di()->get( Remote_Address::class );
$ips = (array) $remote_addr->get_ip_address();
}
return $this->filter_user_ips( $ips );
}
/**
* Get user IP header.
*
* @return string
*/
public function get_user_ip_header(): string {
$ip_header = '';
$service = wd_di()->get( Smart_Ip_Detection::class );
if ( $service->is_smart_ip_detection_enabled() ) {
$ip_detail = $service->get_smart_ip_detection_details();
$ip_header = isset( $ip_detail[1] ) ? $ip_detail[1] : '';
} else {
$model = wd_di()->get( Firewall::class );
$remote_addr = wd_di()->get( Remote_Address::class );
$ip_header = $remote_addr->get_http_ip_header_value( esc_html( $model->http_ip_header ) );
}
return $ip_header;
}
/**
* Checks if an IP address is in the correct format within a given array of IP addresses.
*
* @param string $ip The IP address to check.
* @param array $arr_ips An array of IP addresses to check against.
*
* @return bool Returns true if the IP address is in the correct format within the array, false otherwise.
*/
public function is_ip_in_format( $ip, $arr_ips ): bool {
foreach ( $arr_ips as $wip ) {
if ( false === strpos( $wip, '-' ) && false === strpos( $wip, '/' ) && trim( $wip ) === $ip ) {
return true;
} elseif ( false !== strpos( $wip, '-' ) ) {
$ips = explode( '-', $wip );
if ( $this->compare_in_range( $ip, $ips[0], $ips[1] ) ) {
return true;
}
} elseif ( false !== strpos( $wip, '/' ) && $this->compare_cidr( $ip, $wip ) ) {
return true;
}
}
return false;
}
/**
* Filter user IPs.
* This function takes an array of user IPs, applies the 'defender_user_ip'
* filter to each IP, and returns a unique array of filtered values.
*
* @param array $ips An array of user IPs.
*
* @return array An array of unique, filtered user IPs.
* @since 4.4.2
*/
public function filter_user_ips( array $ips ): array {
$ips_filtered = array();
foreach ( $ips as $ip ) {
/**
* Filters the user IP.
*
* @param string $ip The user IP.
*/
$ips_filtered[] = apply_filters( 'defender_user_ip', $ip );
}
return array_unique( $ips_filtered );
}
/**
* Use $_SERVER['REMOTE_ADDR'] as the first protection layer to avoid spoofed headers.
*
* @param string $blocked_ip The IP address to check.
*
* @return string
*/
public function check_ip_by_remote_addr( $blocked_ip ): string {
$ip_addr = defender_get_data_from_request( 'REMOTE_ADDR', 's' );
return ! empty( $ip_addr ) ? $ip_addr : $blocked_ip;
}
/**
* Check if the given IP is a private IP.
*
* @param string $ip The IP address to check.
*
* @return bool True if the IP is a private IP, false otherwise.
*/
public function is_private_ip( string $ip ): bool {
return filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE ) === false;
}
/**
* Maybe Loopback?
* Don't check '127.0.0.0/8' and '::1/128'.
*
* @return array
*/
public function get_localhost_ips(): array {
return array( '127.0.0.1', '::1' );
}
/**
* Determines if the current request originates from this server.
*
* Validates that the request is a loopback from the server by:
* 1. Matching the IP against localhost, hostname IP, or a whitelisted server public IP.
* 2. Matching the User-Agent against the WordPress core loopback format.
*
* @return bool True if it's a valid server-originated loopback request; false otherwise.
*/
public function request_is_from_server(): bool {
$user_ips = $this->get_user_ip();
if ( empty( $user_ips ) ) {
return false;
}
// Start building list of trusted server IPs.
$trusted_ips = $this->get_localhost_ips();
$server_ip = defender_get_data_from_request( 'SERVER_ADDR', 's' );
if ( $this->check_validate_ip( $server_ip ) ) {
$trusted_ips[] = $server_ip;
}
$stored_ip = get_site_option( \WP_Defender\Component\Firewall::WHITELIST_SERVER_PUBLIC_IP_OPTION, '' );
if ( $this->check_validate_ip( $stored_ip ) ) {
$trusted_ips[] = $stored_ip;
}
/**
* Filters the list of trusted server IP addresses used to validate server-originated loopback requests.
*
* This filter allows developers to customize the list of IP addresses considered as trusted sources
* for internal server requests. These IPs are checked against the request's origin IP to determine
* whether it's a valid server-initiated loopback (e.g., WordPress cron or REST loopback check).
*
* Common use cases:
* - Add public-facing server IPs behind proxies/load balancers.
* - Adjust loopback behavior in containerized or cloud environments.
* - Remove/override default server hostname resolution or localhost IPs.
*
* @since 5.3.0
*
* @param array $trusted_ips An array of trusted server IP addresses.
*/
$trusted_ips = (array) apply_filters( 'wp_defender_server_ips', array_unique( $trusted_ips ) );
// Only return true if the request IP is in the trusted list.
if ( ! empty( array_intersect( $user_ips, $trusted_ips ) ) ) {
return true;
}
// User-Agent missing? Not a valid loopback.
$server_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
if ( empty( $server_agent ) ) {
return false;
}
// User-Agent must match WordPress loopback format.
$expected_ua = 'WordPress/' . wp_get_wp_version() . '; ' . home_url( '/' );
return $server_agent === $expected_ua;
}
}