Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Validator: HostWithPublicIPv4Address #257

Merged
merged 2 commits into from
Jun 14, 2024
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
1 change: 1 addition & 0 deletions docs/book/v2/set.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The following validators come with the laminas-validator distribution.
- [GreaterThan](validators/greater-than.md)
- [Hex](validators/hex.md)
- [Hostname](validators/hostname.md)
- [HostWithPublicIPv4Address](validators/host-with-public-ipv4-address.md)
- [Iban](validators/iban.md)
- [Identical](validators/identical.md)
- [InArray](validators/in-array.md)
Expand Down
39 changes: 39 additions & 0 deletions docs/book/v2/validators/host-with-public-ipv4-address.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Host with Public IPv4 Address Validator

`Laminas\Validator\HostWithPublicIPv4Address` allows you to validate that an IP address is not a reserved address such as 127.0.0.1, or that a hostname does not point to a known, reserved address.

## Supported options

This validator has no options

## Basic usage

```php
$validator = new Laminas\Validator\HostWithPublicIPv4Address();

if ($validator->isValid('example.com')) {
// hostname appears to be valid
} else {
// hostname is invalid; print the reasons
foreach ($validator->getMessages() as $message) {
echo "$message\n";
}
}
```

```php
$validator = new Laminas\Validator\HostWithPublicIPv4Address();

if ($validator->isValid('192.168.0.1')) {
// hostname appears to be valid
} else {
// hostname is invalid; print the reasons
foreach ($validator->getMessages() as $message) {
echo "$message\n";
}
}
```

## Hostnames with multiple records

When validating a hostname as opposed to an IP address, if that hostname resolves to multiple IPv4 addresses and _any_ of those addresses are private or reserved, then the validator will deem the hostname invalid.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ nav:
- GreaterThan: v2/validators/greater-than.md
- Hex: v2/validators/hex.md
- Hostname: v2/validators/hostname.md
- HostWithPublicIPv4Address: v2/validators/host-with-public-ipv4-address.md
- Iban: v2/validators/iban.md
- Identical: v2/validators/identical.md
- InArray: v2/validators/in-array.md
Expand Down
8 changes: 8 additions & 0 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
<code><![CDATA[$this->options]]></code>
<code><![CDATA[$this->options]]></code>
<code><![CDATA[$this->options]]></code>
<code><![CDATA[$this->options]]></code>
</UndefinedThisPropertyAssignment>
</file>
<file src="src/Barcode.php">
Expand Down Expand Up @@ -2537,6 +2538,13 @@
<code><![CDATA[basicDataProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/HostWithPublicIPv4AddressTest.php">
<PossiblyUnusedMethod>
<code><![CDATA[hostnameProvider]]></code>
<code><![CDATA[releasedReservedIpProvider]]></code>
<code><![CDATA[reservedIpProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/HostnameTest.php">
<InvalidArgument>
<code><![CDATA[$option]]></code>
Expand Down
158 changes: 158 additions & 0 deletions src/HostWithPublicIPv4Address.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

namespace Laminas\Validator;

use function explode;
use function filter_var;
use function get_debug_type;
use function gethostbynamel;
use function ip2long;
use function is_array;
use function is_string;
use function pow;

use const FILTER_FLAG_GLOBAL_RANGE;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_NO_PRIV_RANGE;
use const FILTER_FLAG_NO_RES_RANGE;
use const FILTER_VALIDATE_IP;
use const PHP_VERSION_ID;

final class HostWithPublicIPv4Address extends AbstractValidator
{
/**
* Reserved CIDRs are extracted from IANA with additions from Wikipedia
*
* @link https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
* @link https://en.wikipedia.org/wiki/Reserved_IP_addresses
*/
private const RESERVED_CIDR = [
'0.0.0.0/8',
'0.0.0.0/32',
'10.0.0.0/8',
'100.64.0.0/10',
'127.0.0.0/8',
'169.254.0.0/16',
'172.16.0.0/12',
'192.0.0.0/24',
'192.0.0.0/29',
'192.0.0.8/32',
'192.0.0.9/32',
'192.0.0.10/32',
'192.0.0.170/32',
'192.0.0.171/32',
'192.0.2.0/24',
'192.31.196.0/24',
'192.52.193.0/24',
'192.88.99.0/24',
'192.168.0.0/16',
'192.175.48.0/24',
'198.18.0.0/15',
'198.51.100.0/24',
'203.0.113.0/24',
'224.0.0.0/4', // Wikipedia
'233.252.0.0/24', // Wikipedia
'240.0.0.0/4',
'255.255.255.255/32',
];

public const ERROR_NOT_STRING = 'hostnameNotString';
public const ERROR_HOSTNAME_NOT_RESOLVED = 'hostnameNotResolved';
public const ERROR_PRIVATE_IP_FOUND = 'privateIpAddressFound';

/** @var array<non-empty-string, non-empty-string> */
protected array $messageTemplates = [
self::ERROR_NOT_STRING => 'Expected a string hostname but received %type%',
self::ERROR_HOSTNAME_NOT_RESOLVED => 'The hostname "%value%" cannot be resolved',
self::ERROR_PRIVATE_IP_FOUND => 'The hostname "%value%" resolves to at least one reserved IPv4 address',
];

protected string $type = 'null';

/** @var array<non-empty-string, non-empty-string> */
protected array $messageVariables = [
'type' => 'type',
'value' => 'value',
];

public function isValid(mixed $value): bool
{
$this->type = get_debug_type($value);

if (! is_string($value)) {
$this->error(self::ERROR_NOT_STRING);

return false;
}

$this->value = $value;

if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) {
$addressList = gethostbynamel($value);
} else {
$addressList = [$value];
}

if (! is_array($addressList)) {
$this->error(self::ERROR_HOSTNAME_NOT_RESOLVED);

return false;
}

$privateAddressWasFound = false;

$filterFlags = FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE;
if (PHP_VERSION_ID >= 80200) {
/**
* @psalm-var int $filterFlags
* @psalm-suppress UndefinedConstant
*/
$filterFlags |= FILTER_FLAG_GLOBAL_RANGE;
}

foreach ($addressList as $server) {
gsteel marked this conversation as resolved.
Show resolved Hide resolved
/**
* Initially test with PHP's built-in filter_var features as this will be quicker than checking
* presence with a CIDR
*/
if (filter_var($server, FILTER_VALIDATE_IP, $filterFlags) === false) {
$privateAddressWasFound = true;

break;
}

if ($this->inReservedCidr($server)) {
$privateAddressWasFound = true;

break;
}
}

if ($privateAddressWasFound) {
$this->error(self::ERROR_PRIVATE_IP_FOUND);

return false;
}

return true;
}

private function inReservedCidr(string $ip): bool
{
foreach (self::RESERVED_CIDR as $cidr) {
$cidr = explode('/', $cidr);
$startIp = ip2long($cidr[0]);
$endIp = ip2long($cidr[0]) + pow(2, 32 - (int) $cidr[1]) - 1;

$int = ip2long($ip);

if ($int >= $startIp && $int <= $endIp) {
return true;
}
}

return false;
}
}
1 change: 1 addition & 0 deletions src/ValidatorPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ class ValidatorPluginManager extends AbstractPluginManager
GreaterThan::class => InvokableFactory::class,
Hex::class => InvokableFactory::class,
Hostname::class => InvokableFactory::class,
HostWithPublicIPv4Address::class => InvokableFactory::class,
Iban::class => InvokableFactory::class,
Identical::class => InvokableFactory::class,
InArray::class => InvokableFactory::class,
Expand Down
Loading