From e76c99017596fed9d58c0882d40064f63c0ec257 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Fri, 13 Feb 2026 17:13:39 +0700 Subject: [PATCH 1/3] Fix. Code. Syncing RC with library --- .../lib/Cleantalk/Common/Helper.php | 100 +++++++++++++----- .../lib/Cleantalk/Common/RemoteCalls.php | 12 +++ 2 files changed, 88 insertions(+), 24 deletions(-) diff --git a/cleantalk.antispam/lib/Cleantalk/Common/Helper.php b/cleantalk.antispam/lib/Cleantalk/Common/Helper.php index f8b64e0..58d9f47 100644 --- a/cleantalk.antispam/lib/Cleantalk/Common/Helper.php +++ b/cleantalk.antispam/lib/Cleantalk/Common/Helper.php @@ -373,10 +373,17 @@ static function ip__mask__long_to_number($long_mask) */ static public function ip__validate($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::ip__v6_reduce($ip) != '0::0') return 'v6'; // IPv6 - return false; // Unknown + if ( !$ip ) { // NULL || FALSE || '' || so on... + return false; + } + if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && $ip != '0.0.0.0' ) { // IPv4 + return 'v4'; + } + if ( filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && self::ip__v6_reduce($ip) != '0::0' ) { // IPv6 + return 'v6'; + } + + return false; // Unknown } /** @@ -428,10 +435,10 @@ static public function ip__v6_normalize($ip) */ static public function ip__v6_reduce($ip) { - if(strpos($ip, ':') !== false){ + 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; + $ip = strpos($ip, '0') === 0 && substr($ip, 1) !== false ? substr($ip, 1) : $ip; } return $ip; } @@ -466,29 +473,74 @@ static public function ip__resolve__cleantalks($ip) if(self::ip__validate($ip)){ $url = array_search($ip, self::$cleantalks_servers); return $url - ? $url + ? parse_url($url, PHP_URL_HOST) : self::ip__resolve($ip); }else return $ip; } - + /** - * Get URL form IP - * - * @param $ip - * - * @return string - */ - static public function ip__resolve($ip) - { - if(self::ip__validate($ip)){ - $url = gethostbyaddr($ip); - if($url) - return $url; - } - return $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 or false on failure + */ + public static function ip__resolve($ip) + { + // Validate IP first + $ip_version = self::ip__validate($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::ip__v6_normalize($ip); + foreach ($forward_ips as $forward_ip) { + if (self::ip__v6_normalize($forward_ip) === $normalized_ip) { + return $hostname; + } + } + } elseif (in_array($ip, $forward_ips, true)) { + return $hostname; + } + + return false; + } + /** * Resolve DNS to IP * diff --git a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php index eaebd58..6d91ba6 100644 --- a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php +++ b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php @@ -3,6 +3,8 @@ namespace Cleantalk\Common; use Cleantalk\Common\Variables\Get; +use Cleantalk\Common\Variables\Request; +use Cleantalk\Common\Helper; abstract class RemoteCalls { @@ -30,6 +32,16 @@ public function __construct( $api_key ) $this->available_rc_actions = $this->getAvailableRcActions(); } + /** + * @param $name + * @return mixed + * @psalm-taint-source input + */ + public static function getVariable($name) + { + return Request::get($name); + } + /** * Checking if the current request is the Remote Call * From d747f9a0aa58eccf2a49dbe6a92a6e51c1437ed4 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Fri, 13 Feb 2026 17:13:50 +0700 Subject: [PATCH 2/3] Fix. Code. Syncing RC with library --- .../lib/Cleantalk/Common/RemoteCalls.php | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php index 6d91ba6..cd5ceda 100644 --- a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php +++ b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php @@ -47,14 +47,43 @@ public static function getVariable($name) * * @return bool */ - public static function check() + public static function check() { - return - Get::get( 'spbc_remote_call_token' ) && - Get::get( 'spbc_remote_call_action' ) && - Get::get( 'plugin_name' ) && - in_array( Get::get( 'plugin_name' ), array( 'antispam','anti-spam', 'apbct' ) ); - } + if (static::getVariable('spbc_remote_call_action')) { + static::getVariable('spbc_remote_call_token') + ? self::checkWithToken() + : self::checkWithoutToken(); + } + return false; + } + + public static function checkWithToken() + { + return in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')); + } + + public static function checkWithoutToken() + { + global $apbct; + + $rc_servers = [ + 'netserv3.cleantalk.org', + 'netserv4.cleantalk.org', + ]; + // Resolve IP of the client making the request and verify hostname from it to be in the list of RC servers hostnames + $client_ip = Helper::ip__get('remote_addr'); + $verified_hostname = $client_ip ? Helper::ip__resolve($client_ip) : false; + $is_noc_request = ! $apbct->key_is_ok && + in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')) && + $verified_hostname !== false && + in_array($verified_hostname, $rc_servers, true); + + // no token needs for this action, at least for now + // todo Probably we still need to validate this, consult with analytics team + $is_wp_nonce_request = $apbct->key_is_ok && static::getVariable('spbc_remote_call_action') === 'get_fresh_wpnonce'; + + return $is_wp_nonce_request || $is_noc_request; + } /** * Execute corresponding method of RemoteCalls if exists From 60018439e35bd425c6520fdc1dc18bdec32e6360 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 16 Feb 2026 16:35:19 +0700 Subject: [PATCH 3/3] Fix. Code. Left the option to work only with a token --- .../lib/Cleantalk/Common/RemoteCalls.php | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php index cd5ceda..6fbe301 100644 --- a/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php +++ b/cleantalk.antispam/lib/Cleantalk/Common/RemoteCalls.php @@ -52,7 +52,7 @@ public static function check() if (static::getVariable('spbc_remote_call_action')) { static::getVariable('spbc_remote_call_token') ? self::checkWithToken() - : self::checkWithoutToken(); + : false; } return false; } @@ -62,29 +62,6 @@ public static function checkWithToken() return in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')); } - public static function checkWithoutToken() - { - global $apbct; - - $rc_servers = [ - 'netserv3.cleantalk.org', - 'netserv4.cleantalk.org', - ]; - // Resolve IP of the client making the request and verify hostname from it to be in the list of RC servers hostnames - $client_ip = Helper::ip__get('remote_addr'); - $verified_hostname = $client_ip ? Helper::ip__resolve($client_ip) : false; - $is_noc_request = ! $apbct->key_is_ok && - in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')) && - $verified_hostname !== false && - in_array($verified_hostname, $rc_servers, true); - - // no token needs for this action, at least for now - // todo Probably we still need to validate this, consult with analytics team - $is_wp_nonce_request = $apbct->key_is_ok && static::getVariable('spbc_remote_call_action') === 'get_fresh_wpnonce'; - - return $is_wp_nonce_request || $is_noc_request; - } - /** * Execute corresponding method of RemoteCalls if exists *