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..6fbe301 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,19 +32,35 @@ 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 * * @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() + : false; + } + return false; + } + + public static function checkWithToken() + { + return in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')); + } /** * Execute corresponding method of RemoteCalls if exists