Skip to content

Commit 71717ee

Browse files
authored
Merge pull request #733 from CleanTalk/spf_prt_av
Fix. Code. Protects against PTR spoofing
2 parents eb09990 + 0bc0ffe commit 71717ee

File tree

5 files changed

+44
-16
lines changed

5 files changed

+44
-16
lines changed

inc/cleantalk-public-integrations.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,10 @@ function ct_woocommerce_wishlist_check($args)
164164
}
165165
}
166166

167-
//If the IP is a Google bot
168-
$hostname = gethostbyaddr(TT::toString(Server::get('REMOTE_ADDR')));
169-
if ( ! strpos($hostname, 'googlebot.com') ) {
167+
//If the IP is a Google bot (with FCrDNS verification to prevent PTR spoofing)
168+
$client_ip = TT::toString(Server::get('REMOTE_ADDR'));
169+
$verified_hostname = \Cleantalk\Common\Helper::ipResolve($client_ip);
170+
if ( ! $verified_hostname || strpos($verified_hostname, 'googlebot.com') === false ) {
170171
do_action('apbct_skipped_request', __FILE__ . ' -> ' . __FUNCTION__ . '():' . __LINE__, $_POST);
171172

172173
return $args;

lib/Cleantalk/Antispam/Cleantalk.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,8 @@ public function httpPing($host)
479479
$file = @fsockopen($host, 443, $errno, $errstr, $this->max_server_timeout / 1000);
480480
} else {
481481
$http = new Request();
482-
$host = 'https://' . gethostbyaddr($host);
482+
$verified_host = Helper::ipResolve($host);
483+
$host = 'https://' . ($verified_host ?: $host);
483484
$file = $http->setUrl($host)
484485
->setOptions(['timeout' => $this->max_server_timeout / 1000])
485486
->setPresets('get_code get')

lib/Cleantalk/ApbctWP/RemoteCalls.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,14 @@ public static function checkWithoutToken()
5656
'netserv3.cleantalk.org',
5757
'netserv4.cleantalk.org',
5858
];
59-
59+
// Resolve IP of the client making the request and verify hostname from it to be in the list of RC servers hostnames
60+
$client_ip = Helper::ipGet('remote_addr');
61+
$verified_hostname = $client_ip ? \Cleantalk\Common\Helper::ipResolve($client_ip) : false;
6062
$is_noc_request = ! $apbct->key_is_ok &&
6163
Request::get('spbc_remote_call_action') &&
6264
in_array(Request::get('plugin_name'), array('antispam', 'anti-spam', 'apbct')) &&
63-
in_array(Helper::ipResolve(Helper::ipGet('remote_addr')), $rc_servers, true);
65+
$verified_hostname !== false &&
66+
in_array($verified_hostname, $rc_servers, true);
6467

6568
// no token needs for this action, at least for now
6669
// todo Probably we still need to validate this, consult with analytics team

lib/Cleantalk/Common/DNS.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ public static function getResponseTime($host)
114114
$file = @fsockopen($host, 443, $errno, $errstr, static::$max_server_timeout / 1000);
115115
} else {
116116
$http = new Request();
117-
$host = 'https://' . gethostbyaddr($host);
117+
// Use FCrDNS-verified hostname, or IP directly if verification fails
118+
$verified_host = Helper::ipResolve($host);
119+
$host = 'https://' . ($verified_host ?: $host);
118120
$file = $http->setUrl($host)
119121
->setOptions(['timeout' => static::$max_server_timeout / 1000])
120122
->setPresets('get_code get')

lib/Cleantalk/Common/Helper.php

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -528,22 +528,43 @@ public static function ipV6Reduce($ip)
528528
}
529529

530530
/**
531-
* Get URL form IP
531+
* Resolve IP to hostname with FCrDNS (Forward-Confirmed reverse DNS) verification.
532+
* Protects against PTR spoofing by verifying the hostname resolves back to the same IP.
532533
*
533-
* @param $ip
534+
* @param string $ip IP address to resolve
534535
*
535-
* @return string
536+
* @return string|false Verified hostname or false on failure
536537
*/
537538
public static function ipResolve($ip)
538539
{
539-
if (self::ipValidate($ip)) {
540-
$url = gethostbyaddr($ip);
541-
if ($url) {
542-
return $url;
543-
}
540+
// Validate IP first
541+
if (!self::ipValidate($ip)) {
542+
return false;
544543
}
545544

546-
return $ip;
545+
// Reverse DNS lookup (PTR record)
546+
$hostname = gethostbyaddr($ip);
547+
548+
// If gethostbyaddr returns the IP itself, it means no PTR record exists
549+
if (!$hostname || $hostname === $ip) {
550+
return false;
551+
}
552+
553+
// Forward DNS lookup (A/AAAA records) - verify the hostname points back to the IP
554+
$forward_ips = gethostbynamel($hostname);
555+
556+
// If forward lookup fails, we can't verify
557+
if (!$forward_ips) {
558+
return false;
559+
}
560+
561+
// Check if the original IP is in the list of IPs the hostname resolves to
562+
if (in_array($ip, $forward_ips, true)) {
563+
return $hostname;
564+
}
565+
566+
// FCrDNS verification failed - possible PTR spoofing attempt
567+
return false;
547568
}
548569

549570
/**

0 commit comments

Comments
 (0)