Affected versions

Symfony Validator versions <5.4.56, >=6, <6.4.50, >=7, <7.4.23, >=8, <8.1.11 are affected by this security issue.

The issue has been fixed in Symfony Validator 5.4.56, 6.4.50, 7.4.23, 8.1.11.

Description

Symfony\Component\Validator\Constraints\IpValidator decides the public, private and reserved classification for the Ip constraint's filtered modes solely from PHP's FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE flags, with no second check.

Those flags are an incomplete basis for this purpose, in two ways.

Whole ranges are covered by neither flag. 100.64.0.0/10 (RFC 6598 shared address space, which is genuinely routed internal space), the TEST-NET ranges 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, and 198.18.0.0/15 (RFC 2544 benchmarking) are accepted by every mode, as is the entire IPv6 special-purpose family: 6to4 (2002::/16), Teredo (2001::/32), NAT64 (64:ff9b::/96, 64:ff9b:1::/48), documentation (2001:db8::/32) and benchmarking (2001:0002::/48).

Classification is also spelling dependent for IPv6. ::7f00:1 is accepted by every mode, while ::127.0.0.1, the same 128 bits, is rejected by NO_RES from PHP 8.3 onward. PHP 8.3 added a reserved-range rule for ::ffff:0:0/96, so ::ffff:127.0.0.1 is accepted on PHP 8.1 and 8.2 but rejected from 8.3, while the ::7f00:1 spelling and the NO_PRIV mode remain bypassed on every version up to 8.5.

A developer using #[Assert\Ip(version: Ip::ALL_ONLY_PUBLIC)] or Ip::ALL_NO_PRIV as an anti-internal-address guard is therefore bypassed. The strongest cases are IPv4-mapped addresses in ALL_NO_PRIV mode, and carrier-grade NAT space in the public-only modes. A mapped address connects to the RFC 1918 or loopback host it embeds. On PHP 8.3 and later, ALL_ONLY_PUBLIC rejects IPv4-mapped addresses. The IPv4-compatible spelling ::7f00:1 is misclassified on every version, but it does not connect to loopback on Linux:

1
2
3
$validator->validate('::ffff:10.0.0.1', new Ip(version: Ip::ALL_NO_PRIV));  // 0 violations
$validator->validate('2002:7f00:1::', new Ip(version: Ip::ALL_ONLY_PUBLIC));   // 0 violations
$validator->validate('100.64.0.1', new Ip(version: Ip::V4_ONLY_PUBLIC));       // 0 violations

This is an opt-in control: the default version = V4 performs no range filtering at all, so the affected population is precisely those relying on the public, private and reserved modes as a security filter.

Resolution

For the NO_PRIV, NO_RES and ONLY_PUBLIC modes (NO_PRIVATE and NO_RESERVED from Symfony 7.0 on), IpValidator now additionally rejects any address falling in the non-public subnets, mirroring Symfony\Component\HttpFoundation\IpUtils::PRIVATE_SUBNETS for both address families. The check is applied on top of the existing filter_var() call so it can only tighten validation, and it is skipped entirely for the unfiltered V4, V6 and ALL modes, which are unchanged.

Note three deliberate consequences:

  • All three filtered modes reject the whole family, so ONLY_PUBLIC now agrees with IpUtils::isPrivateIp() on every address, and NO_PRIV and NO_RES behave the same as ONLY_PUBLIC. These modes are treated as security filters rather than as a taxonomy of address categories. Applications that relied on NO_PRIV accepting reserved addresses, or on NO_RES accepting private ones, will see previously valid values rejected.
  • Addresses previously accepted are now rejected, including 100.64.0.1 and 192.0.2.1 in every filtered mode, 127.0.0.1 under NO_PRIV, 10.0.0.1 under NO_RES, and ::ffff:8.8.8.8, an IPv4-mapped form of a public address, under NO_PRIV.
  • On a PHP built with --disable-ipv6, inet_pton() cannot parse IPv6 addresses, so the three filtered modes reject every IPv6 address rather than falling back to the previous behaviour. IPv4 addresses and the unfiltered V4, V6 and ALL modes are unaffected.

IpUtils itself could not be reused: symfony/validator does not depend on symfony/http-foundation and must not gain that dependency, so the subnet list is duplicated in the Validator and has to be kept in sync by hand.

The patches for this issue are available here for branch 5.4 (and forward-ported to 6.4, 7.4, 8.1 and 8.2).

Credits

We would like to thank zx (Jace) for reporting the issue.