From 4e277596c4347a0087ac3951621f0000f0779dec Mon Sep 17 00:00:00 2001 From: alexandergull Date: Tue, 3 Mar 2026 18:04:32 +0500 Subject: [PATCH 1/5] Upd. Updated CleanTalk resolve. --- cleantalk/antispam/model/Cleantalk.php | 127 +++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 7 deletions(-) diff --git a/cleantalk/antispam/model/Cleantalk.php b/cleantalk/antispam/model/Cleantalk.php index dda706c..1c5e629 100644 --- a/cleantalk/antispam/model/Cleantalk.php +++ b/cleantalk/antispam/model/Cleantalk.php @@ -506,14 +506,14 @@ private function httpRequest($msg) if ($server['host'] === 'localhost' || $server['ip'] === null) { $work_url = $server['host']; + } else { + $host = self::isCleanTalkServer($server['ip']); + if (!$host) { + continue; + } + $work_url = $url_prefix . $host; } - else - { - $server_host = $server['ip']; - $work_url = $server_host; - } - $host = filter_var($work_url,FILTER_VALIDATE_IP) ? gethostbyaddr($work_url) : $work_url; - $work_url = $url_prefix . $host; + if (isset($url_suffix)) { $work_url = $work_url . $url_suffix; @@ -646,6 +646,119 @@ private function get_servers_ip($host) return $response; } + static public function ipResolve($ip) + { + // Validate IP first + $ip_version = self::ipValidate($ip); + if (!$ip_version) { + return false; + } + + // Reverse DNS lookup (PTR record) + $hostname = gethostbyaddr($ip); + + // If gethostbyaddr returns the IP itself, it means no PTR record exists + if (!$hostname || $hostname === $ip) { + return false; + } + + // Forward DNS lookup - use dns_get_record() to support both IPv4 (A) and IPv6 (AAAA) records + $record_type = ($ip_version === 'v6') ? DNS_AAAA : DNS_A; + $ip_field = ($ip_version === 'v6') ? 'ipv6' : 'ip'; + + $records = @dns_get_record($hostname, $record_type); + + // If forward lookup fails, we can't verify + if (empty($records)) { + return false; + } + + // Extract IPs from DNS records + $forward_ips = array(); + foreach ($records as $record) { + if (isset($record[$ip_field])) { + $forward_ips[] = $record[$ip_field]; + } + } + + if (empty($forward_ips)) { + return false; + } + + // Check if the original IP is in the list of IPs the hostname resolves to + if ($ip_version === 'v6') { + $normalized_ip = self::ipV6Normalize($ip); + foreach ($forward_ips as $forward_ip) { + if (self::ipV6Normalize($forward_ip) === $normalized_ip) { + return $hostname; + } + } + } elseif (in_array($ip, $forward_ips, true)) { + return $hostname; + } + + return false; + } + + /** + * Validating IPv4, IPv6 + * + * @param string $ip + * + * @return string|bool + */ + public static function ipValidate($ip) + { + if ( !$ip ) { + return false; + } // NULL || FALSE || '' || so on... + if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $ip != '0.0.0.0' ) { + return 'v4'; + } // IPv4 + if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && self::ipV6Reduce($ip) != '0::0' ) { + return 'v6'; + } // IPv6 + return false; // Unknown + } + + /** + * Expand IPv6 + * + * @param string $ip + * + * @return string IPv6 + */ + public static function ipV6Normalize($ip) + { + $ip = trim($ip); + // Searching for ::ffff:xx.xx.xx.xx patterns and turn it to IPv6 + if ( preg_match('/^::ffff:([0-9]{1,3}\.?){4}$/', $ip) ) { + $ip = dechex(sprintf("%u", ip2long(substr($ip, 7)))); + $ip = '0:0:0:0:0:0:' . (strlen($ip) > 4 ? substr('abcde', 0, -4) : '0') . ':' . substr($ip, -4, 4); + // Normalizing hextets number + } elseif ( strpos($ip, '::') !== false ) { + $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip); + $ip = strpos($ip, ':') === 0 ? '0' . $ip : $ip; + $ip = strpos(strrev($ip), ':') === 0 ? $ip . '0' : $ip; + } + // Simplifyng hextets + if ( preg_match('/:0(?=[a-z0-9]+)/', $ip) ) { + $ip = preg_replace('/:0(?=[a-z0-9]+)/', ':', strtolower($ip)); + $ip = self::ipV6Normalize($ip); + } + return $ip; + } + + static public function isCleanTalkServer($ip) + { + $pattern = '/^(api|apix[0-9]+|moderate|moderate[0-9]+)\.cleantalk\.(org|ru)$/'; + $validated_host = self::ipResolve($ip); + if ($validated_host && preg_match($pattern, $validated_host)) { + return $validated_host; + } + return false; + } + /** * Function to check response time * param string From 3b212dd70f0e4918cfeefe8fef12ad8893e150e7 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Tue, 5 May 2026 08:13:27 +0300 Subject: [PATCH 2/5] Fix. Common. Method `ipResolve` fixed --- cleantalk/antispam/model/Cleantalk.php | 60 ++++++++++++++++++++------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/cleantalk/antispam/model/Cleantalk.php b/cleantalk/antispam/model/Cleantalk.php index 1c5e629..28e0406 100644 --- a/cleantalk/antispam/model/Cleantalk.php +++ b/cleantalk/antispam/model/Cleantalk.php @@ -507,7 +507,7 @@ private function httpRequest($msg) { $work_url = $server['host']; } else { - $host = self::isCleanTalkServer($server['ip']); + $host = self::ipResolve($server['ip']); if (!$host) { continue; } @@ -646,7 +646,15 @@ private function get_servers_ip($host) return $response; } - static public function ipResolve($ip) + /** + * Resolve IP to hostname with FCrDNS (Forward-Confirmed reverse DNS) verification. + * Protects against PTR spoofing by verifying the hostname resolves back to the same IP. + * + * @param string $ip IP address to resolve + * + * @return string|false Verified hostname, original IP if unverifiable, or false on failure + */ + public static function ipResolve($ip) { // Validate IP first $ip_version = self::ipValidate($ip); @@ -659,17 +667,36 @@ static public function ipResolve($ip) // If gethostbyaddr returns the IP itself, it means no PTR record exists if (!$hostname || $hostname === $ip) { - return false; + return $ip; } - // Forward DNS lookup - use dns_get_record() to support both IPv4 (A) and IPv6 (AAAA) records - $record_type = ($ip_version === 'v6') ? DNS_AAAA : DNS_A; $ip_field = ($ip_version === 'v6') ? 'ipv6' : 'ip'; + $records = []; - $records = @dns_get_record($hostname, $record_type); + // Forward DNS lookup - use dns_get_record() to support both IPv4 (A) and IPv6 (AAAA) records + if ( function_exists('dns_get_record') ) { + $record_type = ($ip_version === 'v6') ? DNS_AAAA : DNS_A; + $dns_records = dns_get_record($hostname, $record_type); + if ( $dns_records !== false ) { + $records = $dns_records; + } + } + + // Another try if first failed (only for v4) + if ( empty($records) && $ip_version === 'v4' && function_exists('gethostbynamel') ) { + $ips_v4 = gethostbynamel($hostname); + if ( $ips_v4 !== false ) { + foreach ( $ips_v4 as $_ip ) { + $records[] = array( + "ip" => $_ip, + "host" => $hostname + ); + } + } + } // If forward lookup fails, we can't verify - if (empty($records)) { + if ( empty($records) ) { return false; } @@ -749,14 +776,21 @@ public static function ipV6Normalize($ip) return $ip; } - static public function isCleanTalkServer($ip) + /** + * Reduce IPv6 + * + * @param string $ip + * + * @return string IPv6 + */ + public static function ipV6Reduce($ip) { - $pattern = '/^(api|apix[0-9]+|moderate|moderate[0-9]+)\.cleantalk\.(org|ru)$/'; - $validated_host = self::ipResolve($ip); - if ($validated_host && preg_match($pattern, $validated_host)) { - return $validated_host; + if ( strpos($ip, ':') !== false ) { + $ip = preg_replace('/:0{1,4}/', ':', $ip); + $ip = preg_replace('/:{2,}/', '::', $ip); + $ip = strpos($ip, '0') === 0 ? substr($ip, 1) : $ip; } - return false; + return $ip; } /** From 177d53c02df99371a2262af14c681cfd042b087e Mon Sep 17 00:00:00 2001 From: Viktor Date: Tue, 5 May 2026 08:26:34 +0300 Subject: [PATCH 3/5] Fix. Common. Getting work url fixed. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cleantalk/antispam/model/Cleantalk.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cleantalk/antispam/model/Cleantalk.php b/cleantalk/antispam/model/Cleantalk.php index 28e0406..82aa4e1 100644 --- a/cleantalk/antispam/model/Cleantalk.php +++ b/cleantalk/antispam/model/Cleantalk.php @@ -505,7 +505,7 @@ private function httpRequest($msg) { if ($server['host'] === 'localhost' || $server['ip'] === null) { - $work_url = $server['host']; + $work_url = $url_prefix . $server['host']; } else { $host = self::ipResolve($server['ip']); if (!$host) { From 8955024e8f64b867fb6da62a3d9799b4e6ee3ebf Mon Sep 17 00:00:00 2001 From: Glomberg Date: Tue, 5 May 2026 08:36:56 +0300 Subject: [PATCH 4/5] Fix. Common. Method `ipV6Normalize` fixed --- cleantalk/antispam/model/Cleantalk.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cleantalk/antispam/model/Cleantalk.php b/cleantalk/antispam/model/Cleantalk.php index 82aa4e1..fa3a3be 100644 --- a/cleantalk/antispam/model/Cleantalk.php +++ b/cleantalk/antispam/model/Cleantalk.php @@ -761,7 +761,7 @@ public static function ipV6Normalize($ip) // Searching for ::ffff:xx.xx.xx.xx patterns and turn it to IPv6 if ( preg_match('/^::ffff:([0-9]{1,3}\.?){4}$/', $ip) ) { $ip = dechex(sprintf("%u", ip2long(substr($ip, 7)))); - $ip = '0:0:0:0:0:0:' . (strlen($ip) > 4 ? substr('abcde', 0, -4) : '0') . ':' . substr($ip, -4, 4); + $ip = '0:0:0:0:0:0:' . (strlen($ip) > 4 ? substr($ip, 0, -4) : '0') . ':' . substr($ip, -4, 4); // Normalizing hextets number } elseif ( strpos($ip, '::') !== false ) { $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip); From 3eabc5b48a23071f47cbe9e58e1786b5d80a56fb Mon Sep 17 00:00:00 2001 From: Glomberg Date: Tue, 5 May 2026 08:48:32 +0300 Subject: [PATCH 5/5] Fix. Common. Method `ipV6Reduce` fixed --- cleantalk/antispam/model/Cleantalk.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cleantalk/antispam/model/Cleantalk.php b/cleantalk/antispam/model/Cleantalk.php index fa3a3be..ebac926 100644 --- a/cleantalk/antispam/model/Cleantalk.php +++ b/cleantalk/antispam/model/Cleantalk.php @@ -787,8 +787,12 @@ public static function ipV6Reduce($ip) { if ( strpos($ip, ':') !== false ) { $ip = preg_replace('/:0{1,4}/', ':', $ip); + if ( preg_match('/^[0-9a-fA-F]{1,4}(?=:)/', $ip, $matches) ) { + $first_hextet = ltrim($matches[0], '0'); + $first_hextet = $first_hextet === '' ? '0' : $first_hextet; + $ip = $first_hextet . substr($ip, strlen($matches[0])); + } $ip = preg_replace('/:{2,}/', '::', $ip); - $ip = strpos($ip, '0') === 0 ? substr($ip, 1) : $ip; } return $ip; }