Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions lib/Cleantalk/Common/Helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,8 @@ public static function ipV6Reduce($ip)
public static function ipResolve($ip)
{
// Validate IP first
if (!self::ipValidate($ip)) {
$ip_version = self::ipValidate($ip);
if (!$ip_version) {
return false;
}

Expand All @@ -550,20 +551,41 @@ public static function ipResolve($ip)
return false;
}

// Forward DNS lookup (A/AAAA records) - verify the hostname points back to the IP
$forward_ips = gethostbynamel($hostname);
// 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 (!$forward_ips) {
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 (in_array($ip, $forward_ips, true)) {
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;
}

// FCrDNS verification failed - possible PTR spoofing attempt
return false;
}

Expand Down
115 changes: 115 additions & 0 deletions tests/Common/HelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,119 @@ public function test_http__multi_request_success() {
$this->assertIsArray( $res );
$this->assertContainsOnly( 'string', $res );
}

/**
* Test ipResolve returns false for invalid IP
*/
public function test_ipResolve_invalid_ip()
{
$this->assertFalse(Helper::ipResolve('invalid'));
$this->assertFalse(Helper::ipResolve('999.999.999.999'));
$this->assertFalse(Helper::ipResolve(''));
$this->assertFalse(Helper::ipResolve('abc.def.ghi.jkl'));
}

/**
* Test ipResolve returns false for null/empty values
*/
public function test_ipResolve_null_empty()
{
$this->assertFalse(Helper::ipResolve(null));
$this->assertFalse(Helper::ipResolve(false));
}

/**
* Test ipResolve returns false for reserved/special IPs (0.0.0.0)
*/
public function test_ipResolve_reserved_ip()
{
$this->assertFalse(Helper::ipResolve('0.0.0.0'));
}

/**
* Test ipResolve with Google DNS (8.8.8.8) - should return hostname
* This is an integration test that requires network access
*
* @group integration
*/
public function test_ipResolve_google_dns_ipv4()
{
$result = Helper::ipResolve('8.8.8.8');

// Google DNS should resolve to dns.google
if ($result !== false) {
$this->assertStringContainsString('dns.google', $result);
} else {
// DNS resolution might fail in some environments
$this->markTestSkipped('DNS resolution not available in this environment');
}
}

/**
* Test ipResolve with private IP - should return false (no PTR record)
*/
public function test_ipResolve_private_ip()
{
// Private IPs typically don't have public PTR records
$result = Helper::ipResolve('192.168.1.1');

// Either false or the IP itself (no PTR) - both are acceptable
$this->assertTrue($result === false || is_string($result));
}

/**
* Test ipResolve with localhost IPv4
*/
public function test_ipResolve_localhost_ipv4()
{
$result = Helper::ipResolve('127.0.0.1');

// Localhost might resolve to 'localhost' or return false depending on system config
$this->assertTrue($result === false || is_string($result));
}

/**
* Test ipResolve with IPv6 localhost (::1)
*/
public function test_ipResolve_localhost_ipv6()
{
// IPv6 localhost is considered invalid (0::0) by ipValidate
$result = Helper::ipResolve('::1');

$this->assertTrue($result === false || is_string($result));
}

/**
* Test ipResolve with Google DNS IPv6 (2001:4860:4860::8888)
*
* @group integration
*/
public function test_ipResolve_google_dns_ipv6()
{
$result = Helper::ipResolve('2001:4860:4860::8888');

// Google DNS IPv6 should resolve to dns.google
if ($result !== false) {
$this->assertStringContainsString('dns.google', $result);
} else {
// IPv6 DNS resolution might not work in all environments
$this->markTestSkipped('IPv6 DNS resolution not available in this environment');
}
}

/**
* Test ipResolve returns string hostname on success
*
* @group integration
*/
public function test_ipResolve_return_type()
{
$result = Helper::ipResolve('8.8.8.8');

// Result should be either false or a non-empty string
$this->assertTrue(
$result === false || (is_string($result) && strlen($result) > 0),
'ipResolve should return false or a non-empty string hostname'
);
}
}
Loading