Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Validator] Add additional versions (*_NO_PUBLIC, *_ONLY_PRIV & *_ONLY_RES) in IP address & CIDR constraint #52658

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

Merged
merged 1 commit into from
Feb 3, 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
2 changes: 2 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ CHANGELOG

* Add support for `Stringable` values when using the `Cidr`, `CssColor`, `ExpressionSyntax` and `PasswordStrength` constraints
* Add `MacAddress` constraint
* Add `*_NO_PUBLIC`, `*_ONLY_PRIVATE` and `*_ONLY_RESERVED` versions to `Ip` constraint
* Possibility to use all `Ip` constraint versions for `Cidr` constraint
* Add `list` and `associative_array` types to `Type` constraint
* Add the `Charset` constraint

Expand Down
44 changes: 36 additions & 8 deletions src/Symfony/Component/Validator/Constraints/Cidr.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\InvalidArgumentException;

/**
* Validates that a value is a valid CIDR notation.
Expand All @@ -21,6 +22,7 @@
*
* @author Sorin Pop <[email protected]>
* @author Calin Bolea <[email protected]>
* @author Ninos Ego <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Cidr extends Constraint
Expand All @@ -34,9 +36,33 @@ class Cidr extends Constraint
];

private const NET_MAXES = [
Ip::ALL => 128,
Ip::V4 => 32,
Ip::V6 => 128,
Ip::ALL => 128,

Ip::V4_NO_PUBLIC => 32,
Ip::V6_NO_PUBLIC => 128,
Ip::ALL_NO_PUBLIC => 128,

Ip::V4_NO_PRIVATE => 32,
Ip::V6_NO_PRIVATE => 128,
Ip::ALL_NO_PRIVATE => 128,

Ip::V4_NO_RESERVED => 32,
Ip::V6_NO_RESERVED => 128,
Ip::ALL_NO_RESERVED => 128,

Ip::V4_ONLY_PUBLIC => 32,
Ip::V6_ONLY_PUBLIC => 128,
Ip::ALL_ONLY_PUBLIC => 128,

Ip::V4_ONLY_PRIVATE => 32,
Ip::V6_ONLY_PRIVATE => 128,
Ip::ALL_ONLY_PRIVATE => 128,

Ip::V4_ONLY_RESERVED => 32,
Ip::V6_ONLY_RESERVED => 128,
Ip::ALL_ONLY_RESERVED => 128,
];

public string $version = Ip::ALL;
Expand All @@ -45,13 +71,9 @@ class Cidr extends Constraint
public int $netmaskMin = 0;
public int $netmaskMax;

/**
* @param array<string,mixed>|null $options
* @param string|null $version The CIDR version to validate (4, 6 or all, defaults to all)
* @param int|null $netmaskMin The lowest valid for a valid netmask (defaults to 0)
* @param int|null $netmaskMax The biggest valid for a valid netmask (defaults to 32 for IPv4, 128 for IPv6)
* @param string[]|null $groups
*/
/** @var callable|null */
public $normalizer;

public function __construct(
?array $options = null,
?string $version = null,
Expand All @@ -60,6 +82,7 @@ public function __construct(
?string $message = null,
?array $groups = null,
$payload = null,
?callable $normalizer = null,
) {
$this->version = $version ?? $options['version'] ?? $this->version;

Expand All @@ -70,13 +93,18 @@ public function __construct(
$this->netmaskMin = $netmaskMin ?? $options['netmaskMin'] ?? $this->netmaskMin;
$this->netmaskMax = $netmaskMax ?? $options['netmaskMax'] ?? self::NET_MAXES[$this->version];
$this->message = $message ?? $this->message;
$this->normalizer = $normalizer ?? $this->normalizer;

unset($options['netmaskMin'], $options['netmaskMax'], $options['version']);

if ($this->netmaskMin < 0 || $this->netmaskMax > self::NET_MAXES[$this->version] || $this->netmaskMin > $this->netmaskMax) {
throw new ConstraintDefinitionException(sprintf('The netmask range must be between 0 and %d.', self::NET_MAXES[$this->version]));
}

if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));
}

parent::__construct($options, $groups, $payload);
}
}
28 changes: 19 additions & 9 deletions src/Symfony/Component/Validator/Constraints/CidrValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;

/**
* Validates whether a value is a CIDR notation.
*
* @author Sorin Pop <[email protected]>
* @author Calin Bolea <[email protected]>
* @author Ninos Ego <[email protected]>
*/
class CidrValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint): void
Expand All @@ -28,10 +35,16 @@ public function validate($value, Constraint $constraint): void
return;
}

if (!\is_string($value) && !$value instanceof \Stringable) {
if (!\is_scalar($value) && !$value instanceof \Stringable) {
throw new UnexpectedValueException($value, 'string');
}

$value = (string) $value;

if (null !== $constraint->normalizer) {
$value = ($constraint->normalizer)($value);
}

$cidrParts = explode('/', $value, 2);

if (!isset($cidrParts[1])
Expand All @@ -49,14 +62,7 @@ public function validate($value, Constraint $constraint): void
$ipAddress = $cidrParts[0];
$netmask = (int) $cidrParts[1];

$validV4 = Ip::V6 !== $constraint->version
&& filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)
&& $netmask <= 32;

$validV6 = Ip::V4 !== $constraint->version
&& filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6);

if (!$validV4 && !$validV6) {
if (!IpValidator::checkIP($ipAddress, $constraint->version)) {
$this->context
->buildViolation($constraint->message)
->setCode(Cidr::INVALID_CIDR_ERROR)
Expand All @@ -65,6 +71,10 @@ public function validate($value, Constraint $constraint): void
return;
}

if (filter_var($ipAddress, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) && $constraint->netmaskMax > 32) {
$constraint->netmaskMax = 32;
}

if ($netmask < $constraint->netmaskMin || $netmask > $constraint->netmaskMax) {
$this->context
->buildViolation($constraint->netmaskRangeViolationMessage)
Expand Down
58 changes: 46 additions & 12 deletions src/Symfony/Component/Validator/Constraints/Ip.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*
* @author Bernhard Schussek <[email protected]>
* @author Joseph Bielawski <[email protected]>
* @author Ninos Ego <[email protected]>
*/
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Ip extends Constraint
Expand All @@ -28,39 +29,72 @@ class Ip extends Constraint
public const V6 = '6';
public const ALL = 'all';

// adds inverse FILTER_FLAG_NO_RES_RANGE and FILTER_FLAG_NO_PRIV_RANGE flags (skip both)
public const V4_NO_PUBLIC = '4_no_public';
public const V6_NO_PUBLIC = '6_no_public';
public const ALL_NO_PUBLIC = 'all_no_public';

// adds FILTER_FLAG_NO_PRIV_RANGE flag (skip private ranges)
public const V4_NO_PRIV = '4_no_priv';
public const V6_NO_PRIV = '6_no_priv';
public const ALL_NO_PRIV = 'all_no_priv';
public const V4_NO_PRIVATE = '4_no_priv';
public const V4_NO_PRIV = self::V4_NO_PRIVATE; // BC: Alias
public const V6_NO_PRIVATE = '6_no_priv';
public const V6_NO_PRIV = self::V6_NO_PRIVATE; // BC: Alias
public const ALL_NO_PRIVATE = 'all_no_priv';
public const ALL_NO_PRIV = self::ALL_NO_PRIVATE; // BC: Alias

// adds FILTER_FLAG_NO_RES_RANGE flag (skip reserved ranges)
public const V4_NO_RES = '4_no_res';
public const V6_NO_RES = '6_no_res';
public const ALL_NO_RES = 'all_no_res';
public const V4_NO_RESERVED = '4_no_res';
public const V4_NO_RES = self::V4_NO_RESERVED; // BC: Alias
public const V6_NO_RESERVED = '6_no_res';
public const V6_NO_RES = self::V6_NO_RESERVED; // BC: Alias
public const ALL_NO_RESERVED = 'all_no_res';
public const ALL_NO_RES = self::ALL_NO_RESERVED; // BC: Alias

// adds FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE flags (skip both)
public const V4_ONLY_PUBLIC = '4_public';
public const V6_ONLY_PUBLIC = '6_public';
public const ALL_ONLY_PUBLIC = 'all_public';

// adds inverse FILTER_FLAG_NO_PRIV_RANGE
public const V4_ONLY_PRIVATE = '4_private';
public const V6_ONLY_PRIVATE = '6_private';
public const ALL_ONLY_PRIVATE = 'all_private';

// adds inverse FILTER_FLAG_NO_RES_RANGE
public const V4_ONLY_RESERVED = '4_reserved';
public const V6_ONLY_RESERVED = '6_reserved';
public const ALL_ONLY_RESERVED = 'all_reserved';

public const INVALID_IP_ERROR = 'b1b427ae-9f6f-41b0-aa9b-84511fbb3c5b';

protected const VERSIONS = [
self::V4,
self::V6,
self::ALL,

self::V4_NO_PRIV,
self::V6_NO_PRIV,
self::ALL_NO_PRIV,
self::V4_NO_PUBLIC,
self::V6_NO_PUBLIC,
self::ALL_NO_PUBLIC,

self::V4_NO_RES,
self::V6_NO_RES,
self::ALL_NO_RES,
self::V4_NO_PRIVATE,
self::V6_NO_PRIVATE,
self::ALL_NO_PRIVATE,

self::V4_NO_RESERVED,
self::V6_NO_RESERVED,
self::ALL_NO_RESERVED,

self::V4_ONLY_PUBLIC,
self::V6_ONLY_PUBLIC,
self::ALL_ONLY_PUBLIC,

self::V4_ONLY_PRIVATE,
self::V6_ONLY_PRIVATE,
self::ALL_ONLY_PRIVATE,

self::V4_ONLY_RESERVED,
self::V6_ONLY_RESERVED,
self::ALL_ONLY_RESERVED,
];

protected const ERROR_NAMES = [
Expand Down
58 changes: 42 additions & 16 deletions src/Symfony/Component/Validator/Constraints/IpValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,50 @@
*
* @author Bernhard Schussek <[email protected]>
* @author Joseph Bielawski <[email protected]>
* @author Ninos Ego <[email protected]>
*/
class IpValidator extends ConstraintValidator
{
/**
* Checks whether an IP address is valid.
*
* @internal
*/
public static function checkIp(string $ip, mixed $version): bool
{
$flag = match ($version) {
Ip::V4, Ip::V4_NO_PUBLIC, Ip::V4_ONLY_PRIVATE, Ip::V4_ONLY_RESERVED => \FILTER_FLAG_IPV4,
Ip::V6, Ip::V6_NO_PUBLIC, Ip::V6_ONLY_PRIVATE, Ip::V6_ONLY_RESERVED => \FILTER_FLAG_IPV6,
Ip::V4_NO_PRIVATE => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE,
Ip::V6_NO_PRIVATE => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE,
Ip::ALL_NO_PRIVATE => \FILTER_FLAG_NO_PRIV_RANGE,
Ip::V4_NO_RESERVED => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_RES_RANGE,
Ip::V6_NO_RESERVED => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_RES_RANGE,
Ip::ALL_NO_RESERVED => \FILTER_FLAG_NO_RES_RANGE,
Ip::V4_ONLY_PUBLIC => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
Ip::V6_ONLY_PUBLIC => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
Ip::ALL_ONLY_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
default => 0,
};

if (!filter_var($ip, \FILTER_VALIDATE_IP, $flag)) {
return false;
}

$inverseFlag = match ($version) {
Ip::V4_NO_PUBLIC, Ip::V6_NO_PUBLIC, Ip::ALL_NO_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
Ip::V4_ONLY_PRIVATE, Ip::V6_ONLY_PRIVATE, Ip::ALL_ONLY_PRIVATE => \FILTER_FLAG_NO_PRIV_RANGE,
Ip::V4_ONLY_RESERVED, Ip::V6_ONLY_RESERVED, Ip::ALL_ONLY_RESERVED => \FILTER_FLAG_NO_RES_RANGE,
default => 0,
};

if ($inverseFlag && filter_var($ip, \FILTER_VALIDATE_IP, $inverseFlag)) {
return false;
}

return true;
}

public function validate(mixed $value, Constraint $constraint): void
{
if (!$constraint instanceof Ip) {
Expand All @@ -44,22 +85,7 @@ public function validate(mixed $value, Constraint $constraint): void
$value = ($constraint->normalizer)($value);
}

$flag = match ($constraint->version) {
Ip::V4 => \FILTER_FLAG_IPV4,
Ip::V6 => \FILTER_FLAG_IPV6,
Ip::V4_NO_PRIV => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE,
Ip::V6_NO_PRIV => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE,
Ip::ALL_NO_PRIV => \FILTER_FLAG_NO_PRIV_RANGE,
Ip::V4_NO_RES => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_RES_RANGE,
Ip::V6_NO_RES => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_RES_RANGE,
Ip::ALL_NO_RES => \FILTER_FLAG_NO_RES_RANGE,
Ip::V4_ONLY_PUBLIC => \FILTER_FLAG_IPV4 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
Ip::V6_ONLY_PUBLIC => \FILTER_FLAG_IPV6 | \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
Ip::ALL_ONLY_PUBLIC => \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE,
default => 0,
};

if (!filter_var($value, \FILTER_VALIDATE_IP, $flag)) {
if (!self::checkIp($value, $constraint->version)) {
$this->context->buildViolation($constraint->message)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(Ip::INVALID_IP_ERROR)
Expand Down
10 changes: 9 additions & 1 deletion src/Symfony/Component/Validator/Tests/Constraints/CidrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ public function testForV6()

public function testWithInvalidVersion()
{
$availableVersions = [Ip::ALL, Ip::V4, Ip::V6];
$availableVersions = [
Ip::V4, Ip::V6, Ip::ALL,
Ip::V4_NO_PUBLIC, Ip::V6_NO_PUBLIC, Ip::ALL_NO_PUBLIC,
Ip::V4_NO_PRIVATE, Ip::V6_NO_PRIVATE, Ip::ALL_NO_PRIVATE,
Ip::V4_NO_RESERVED, Ip::V6_NO_RESERVED, Ip::ALL_NO_RESERVED,
Ip::V4_ONLY_PUBLIC, Ip::V6_ONLY_PUBLIC, Ip::ALL_ONLY_PUBLIC,
Ip::V4_ONLY_PRIVATE, Ip::V6_ONLY_PRIVATE, Ip::ALL_ONLY_PRIVATE,
Ip::V4_ONLY_RESERVED, Ip::V6_ONLY_RESERVED, Ip::ALL_ONLY_RESERVED,
];

self::expectException(ConstraintDefinitionException::class);
self::expectExceptionMessage(sprintf('The option "version" must be one of "%s".', implode('", "', $availableVersions)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function testExpectsStringCompatibleType()
{
$this->expectException(UnexpectedValueException::class);

$this->validator->validate(123456, new Cidr());
$this->validator->validate([123456], new Cidr());
}

/**
Expand Down Expand Up @@ -205,7 +205,6 @@ public static function getWithInvalidNetmask(): array
return [
['192.168.1.0/-1'],
['0.0.0.0/foobar'],
['10.0.0.0/128'],
['123.45.67.178/aaa'],
['172.16.0.0//'],
['255.255.255.255/1/4'],
Expand All @@ -223,7 +222,6 @@ public static function getWithInvalidMasksAndIps(): array
{
return [
['0.0.0.0/foobar'],
['10.0.0.0/128'],
['123.45.67.178/aaa'],
['172.16.0.0//'],
['172.16.0.0/a/'],
Expand All @@ -243,6 +241,7 @@ public static function getOutOfRangeNetmask(): array
{
return [
['10.0.0.0/24', Ip::V4, 10, 20],
['10.0.0.0/128'],
['2001:0DB8:85A3:0000:0000:8A2E:0370:7334/24', Ip::V6, 10, 20],
];
}
Expand Down
Loading