Skip to content

Commit

Permalink
Merge pull request #257 from gsteel/v2/reserved-ip-v4-validator
Browse files Browse the repository at this point in the history
Add new Validator: `HostWithPublicIPv4Address`
  • Loading branch information
gsteel authored Jun 14, 2024
2 parents 715e115 + 2dc340c commit 1e24e70
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 0 deletions.
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) {
/**
* 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

0 comments on commit 1e24e70

Please sign in to comment.