= 0 && $prefix <= 32; } if ( false !== filter_var( $address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { return $prefix >= 0 && $prefix <= 128; } return false; } /** * Resolve the source IP of the current request. * * Uses `REMOTE_ADDR` by default. When `genius_commerce_trust_forwarded_for` is enabled * and an `X-Forwarded-For` header is present, the rightmost entry of XFF * is preferred — that's the IP added by the *trusted* reverse proxy that * the admin has acknowledged. Earlier entries in XFF are attacker-supplied * and ignored. * * @return string The resolved IP, or empty string if nothing usable. */ public static function get_source_ip(): string { $remote = isset( $_SERVER['REMOTE_ADDR'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : ''; $trust_xff = 'yes' === get_option( self::OPTION_TRUST_XFF, 'no' ); if ( ! $trust_xff ) { return $remote; } if ( empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { return $remote; } // Rightmost = added by the closest (and presumed trusted) proxy. $xff = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ); $parts = array_map( 'trim', explode( ',', $xff ) ); $candidate = end( $parts ); if ( false !== filter_var( $candidate, FILTER_VALIDATE_IP ) ) { return $candidate; } return $remote; } /** * Match a source IP against a single allowlist entry. * * @param string $ip Source IP (already validated). * @param string $entry Allowlist entry (already validated). * @return bool True on match. */ private static function matches( string $ip, string $entry ): bool { // Exact match — works for both v4 and v6 after normalization. if ( false === strpos( $entry, '/' ) ) { return self::normalize( $ip ) === self::normalize( $entry ); } list( $range, $prefix ) = explode( '/', $entry, 2 ); $prefix = (int) $prefix; $is_ipv4_entry = false !== filter_var( $range, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ); $is_ipv4_ip = false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ); if ( $is_ipv4_entry && $is_ipv4_ip ) { return self::cidr_match_v4( $ip, $range, $prefix ); } $is_ipv6_entry = false !== filter_var( $range, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ); $is_ipv6_ip = false !== filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ); if ( $is_ipv6_entry && $is_ipv6_ip ) { return self::cidr_match_v6( $ip, $range, $prefix ); } // Mixed v4/v6 entries don't match each other. return false; } /** * IPv4 CIDR match via ip2long + bitmask. * * @param string $ip Source IPv4. * @param string $range Range start (IPv4). * @param int $prefix Prefix length (0-32). * @return bool */ private static function cidr_match_v4( string $ip, string $range, int $prefix ): bool { $ip_long = ip2long( $ip ); $range_long = ip2long( $range ); if ( false === $ip_long || false === $range_long ) { return false; } if ( 0 === $prefix ) { return true; } // 32-bit mask. Cast to unsigned-ish via & 0xFFFFFFFF for safety on // 64-bit PHP where left-shift can produce signed values. $mask = ( -1 << ( 32 - $prefix ) ) & 0xFFFFFFFF; return ( $ip_long & $mask ) === ( $range_long & $mask ); } /** * IPv6 CIDR match via inet_pton + per-byte bitmask. * * @param string $ip Source IPv6. * @param string $range Range start (IPv6). * @param int $prefix Prefix length (0-128). * @return bool */ private static function cidr_match_v6( string $ip, string $range, int $prefix ): bool { $ip_bin = inet_pton( $ip ); $range_bin = inet_pton( $range ); if ( false === $ip_bin || false === $range_bin ) { return false; } $bytes_to_compare = intdiv( $prefix, 8 ); $bits_remaining = $prefix % 8; if ( $bytes_to_compare > 0 && substr( $ip_bin, 0, $bytes_to_compare ) !== substr( $range_bin, 0, $bytes_to_compare ) ) { return false; } if ( 0 === $bits_remaining ) { return true; } $mask = chr( 0xFF << ( 8 - $bits_remaining ) & 0xFF ); $ip_byte = $ip_bin[ $bytes_to_compare ] ?? "\0"; $range_byte = $range_bin[ $bytes_to_compare ] ?? "\0"; return ( $ip_byte & $mask ) === ( $range_byte & $mask ); } /** * Normalize an IP for exact-match comparison. * * For IPv6, `inet_pton` followed by `inet_ntop` collapses different * textual representations of the same address (e.g. `::1` vs `0:0::1`) * to a canonical form. For IPv4 the textual form is already canonical. * * @param string $ip Source IP. * @return string Canonical text form, or the input on failure. */ private static function normalize( string $ip ): string { $packed = @inet_pton( $ip ); if ( false === $packed ) { return $ip; } $back = @inet_ntop( $packed ); return false === $back ? $ip : $back; } }