Skip to content

ext/filter: Fix FILTER_VALIDATE_IP range flag bypasses through IPv6 notations - #25426

Open
xavierleune wants to merge 3 commits into
php:PHP-8.4from
xavierleune:fix/filter-ipv6-dotted-suffix
Open

xavierleune wants to merge 3 commits into
php:PHP-8.4from
xavierleune:fix/filter-ipv6-dotted-suffix

Conversation

@xavierleune

@xavierleune xavierleune commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Target: PHP-8.4

FILTER_FLAG_NO_PRIV_RANGE, FILTER_FLAG_NO_RES_RANGE and FILTER_FLAG_GLOBAL_RANGE are commonly used to keep user-supplied addresses away from internal networks (SSRF), but several IPv6 notations of internal addresses pass them. These are "Low" issues under the security policy, hence fixed as regular bugs. Almost all changes make the flags reject more addresses, which cannot open a bypass.

filter_var('fc00::1.2.3.4', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);       // passes (fc00::102:304 does not)
filter_var('::ffff:10.0.0.1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);     // passes, reaches 10.0.0.1 from a dual-stack socket
filter_var('64:ff9b::a9fe:a9fe', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE);   // passes, reaches 169.254.169.254 through NAT64
filter_var('fec0::1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE);             // passes, site-local

Commits

  1. Dotted IPv4 suffix. The parser stored every X::a.b.c.d as ::ffff:a.b.c.d, discarding the hexadecimal prefix. Blocks are now stored at their real index; validation itself is unchanged. ::/96 is marked reserved so that ::a.b.c.d stays rejected, and only 64:ff9b::/96 is global, not the whole /32.
  2. Embedded IPv4 addresses. The flags now look at the embedded IPv4 address:
    • NAT64 64:ff9b::/96: classified as the IPv4 address itself (RFC 6052 §3.1).
    • IPv4-mapped, IPv4-compatible and IPv4-translated (::ffff:0:0:0/96): reserved, and private if the IPv4 address is.
    • 6to4 2002::/16: still non-global, private or reserved if the IPv4 address is.
    • 64:ff9b:1::/48 (RFC 8215): private, since the IPv4 position depends on the operator.
    • Teredo is unchanged: its client address is obfuscated and public by design.
  3. Missing blocks.
Block RFC Classification
fec0::/10 site-local (deprecated, still used internally) 3879 private
ff00::/8, 224.0.0.0/4 multicast 4291, 5771 reserved
3fff::/20 documentation 9637 not global
5f00::/16 SRv6 SIDs 9602 not global

Behaviour changes

origin/PHP-8.4 → this branch. ✅ accepted, ❌ rejected.

Address GLOBAL_RANGE NO_RES_RANGE NO_PRIV_RANGE
fc00::1.2.3.4 ❌ ❌ → ✅ ✅ → ❌
2606:4700::8.8.8.8, 64:ff9b::8.8.8.8 ❌ → ✅ ❌ → ✅ ✅
2001:db8::1.2.3.4 ❌ ❌ → ✅ ✅
64:ff9b::10.0.0.1 ❌ ❌ → ✅ ✅ → ❌
64:ff9b::a00:1 ✅ → ❌ ✅ ✅ → ❌
64:ff9b::a9fe:a9fe, 64:ff9b::7f00:1 ✅ → ❌ ✅ → ❌ ✅
64:ff9b:1::a00:1 ✅ → ❌ ✅ ✅ → ❌
2002:a00:1:: (6to4 of 10.0.0.1) ❌ ✅ ✅ → ❌
2002:7f00:1:: (6to4 of 127.0.0.1) ❌ ✅ → ❌ ✅
::ffff:10.0.0.1, ::10.0.0.1, ::ffff:0:10.0.0.1 ❌ ❌ ✅ → ❌
::7f00:1, ::808:808, ::ffff:0:7f00:1 ✅ → ❌ ✅ → ❌ ✅
1::127.0.0.1, ::1:127.0.0.1 ❌ → ✅ ❌ → ✅ ✅
fec0::1 ✅ → ❌ ✅ ✅ → ❌
ff02::1, 224.0.0.1 ✅ → ❌ ✅ → ❌ ✅
3fff::1, 5f00::1 ✅ → ❌ ✅ ✅

Addresses that become accepted are either dotted forms now classified like their hexadecimal form, which already passed, or actually global addresses. None leads to an internal network. CGNAT (100.64.0.0/10) is intentionally unchanged.

Notes

  • bug61700.phpt expected ::ffff:192.168.1.1 to pass NO_PRIV_RANGE, the opposite of what #61700 reported and requinix confirmed. Fix #61700: FILTER_FLAG_IPV6/FILTER_FLAG_NO_PRIV|RES_RANGE failing #7476 was a refactoring that, per its description, "does not fix what we consider as reserved and private, respectively, but merely tries to keep what we had so far". This PR completes #61700.
  • bug77221.phpt: 64:ff9b:: and 64:ff9b::ffff embed 0.0.0.0/8 and are no longer global; two really global addresses were added. filter_ipv4_rfc6890.phpt: 224.0.0.0/4 gets its own case.
  • Checked outside the tests: validation is identical on 400,000 random inputs, and 163,502 dotted addresses classify like their inet_pton() hex form (298,181 mismatches before).

_php_filter_validate_ipv6() filled ip[] as ::ffff:a.b.c.d for every
address with a dotted IPv4 suffix, discarding the hexadecimal prefix,
whose blocks were also stored two positions off. The range flags were
then checked against the wrong address, so fc00::1.2.3.4 passed
FILTER_FLAG_NO_PRIV_RANGE while fc00::102:304 did not.

With an IPv4 suffix, the hexadecimal part is now limited to 6 blocks,
stored at their real index, and the IPv4 part goes to ip[6] and ip[7].
Validation itself is unchanged. The deprecated IPv4-compatible block
::/96 is marked reserved, so that ::a.b.c.d keeps being rejected by
FILTER_FLAG_GLOBAL_RANGE and FILTER_FLAG_NO_RES_RANGE.

Also, only 64:ff9b::/96 (RFC 6052) is global, not the whole
64:ff9b::/32: the local-use prefix 64:ff9b:1::/48 (RFC 8215) is not
globally reachable.
Some IPv6 addresses lead to the IPv4 address they embed: IPv4-mapped
addresses through dual-stack sockets, NAT64 addresses through a NAT64
gateway. The range flags never looked at that IPv4 address, so
::ffff:10.0.0.1 passed FILTER_FLAG_NO_PRIV_RANGE and 64:ff9b::a9fe:a9fe
(169.254.169.254) passed every flag.

- 64:ff9b::/96 is classified as its embedded IPv4 address (RFC 6052
  3.1: the well-known prefix must not embed non-global IPv4 addresses).
- 6to4 (2002::/16) stays non-global, and is private or reserved if its
  embedded IPv4 address is.
- IPv4-mapped, IPv4-compatible and IPv4-translated (::ffff:0:0:0/96)
  addresses are reserved, and private if their IPv4 address is.
- The position of the IPv4 address in 64:ff9b:1::/48 (RFC 8215)
  depends on the operator, so the local-use prefix is private.

This completes the fix of bug #61700, whose report expected
::ffff:192.168.1.1 to fail FILTER_FLAG_NO_PRIV_RANGE. phpGH-7476 only
kept the existing classification: "Note that this patch does not fix
what we consider as reserved and private, respectively, but merely
tries to keep what we had so far."
fec0::1 passed FILTER_FLAG_NO_PRIV_RANGE, and ff02::1 and 224.0.0.1
passed FILTER_FLAG_NO_RES_RANGE.

- fec0::/10 (RFC 3879, deprecated site-local, still used on internal
  networks) is private.
- ff00::/8 and 224.0.0.0/4 (multicast) are reserved.
- 3fff::/20 (RFC 9637, documentation) and 5f00::/16 (RFC 9602, SRv6
  SIDs) are not global.

filter_ipv4_rfc6890.phpt used 224.0.0.0 as the lower bound of
240.0.0.0/4; 224.0.0.0/4 now has its own case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant