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 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_PUBLICnow agrees withIpUtils::isPrivateIp()on every address, andNO_PRIVandNO_RESbehave the same asONLY_PUBLIC. These modes are treated as security filters rather than as a taxonomy of address categories. Applications that relied onNO_PRIVaccepting reserved addresses, or onNO_RESaccepting private ones, will see previously valid values rejected. - Addresses previously accepted are now rejected, including
100.64.0.1and192.0.2.1in every filtered mode,127.0.0.1underNO_PRIV,10.0.0.1underNO_RES, and::ffff:8.8.8.8, an IPv4-mapped form of a public address, underNO_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 unfilteredV4,V6andALLmodes 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.