From adc3759a52033efa5e9d197786a7e6a64610a2af Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Thu, 12 Nov 2026 17:02:38 +0100 Subject: [PATCH 1/3] Fix FILTER_VALIDATE_IP range flags with a dotted IPv4 suffix _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. --- NEWS | 5 ++ ext/filter/logical_filters.c | 38 +++++----- .../tests/filter_ipv6_dotted_suffix.phpt | 67 +++++++++++++++++ .../tests/filter_ipv6_nat64_local_use.phpt | 73 +++++++++++++++++++ 4 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 ext/filter/tests/filter_ipv6_dotted_suffix.phpt create mode 100644 ext/filter/tests/filter_ipv6_nat64_local_use.phpt diff --git a/NEWS b/NEWS index b5dbe65a83ca..f37f1651720b 100644 --- a/NEWS +++ b/NEWS @@ -49,6 +49,11 @@ PHP NEWS - Filter: . Fixed stack overflow in filter_var() and filter_var_array() with deeply nested arrays. (Eyüp Can Akman) + . Fixed FILTER_VALIDATE_IP range flags checking IPv6 addresses with a + dotted IPv4 suffix (e.g. fc00::1.2.3.4) as ::ffff:a.b.c.d. The deprecated + IPv4-compatible block ::/96 is now reserved. (Xavier Leune) + . Fixed FILTER_FLAG_GLOBAL_RANGE accepting the NAT64 local-use prefix + 64:ff9b:1::/48. (Xavier Leune) - MBString: . Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c index 634496027c8b..b73e8be5253e 100644 --- a/ext/filter/logical_filters.c +++ b/ext/filter/logical_filters.c @@ -762,6 +762,7 @@ static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) { int compressed_pos = -1; int blocks = 0; + int max_blocks = 8; unsigned int num, n; int i; const char *ipv4; @@ -794,7 +795,8 @@ static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) str_len--; } - blocks = 2; + /* the IPv4 part takes up the last two 16-bit blocks */ + max_blocks = 6; } end = str + str_len; @@ -809,12 +811,12 @@ static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) if (compressed_pos >= 0) { return 0; } - if (ip && blocks < 8) { + if (ip && blocks < max_blocks) { ip[blocks] = -1; } compressed_pos = blocks++; /* :: means 1 or more 16-bit 0 blocks */ if (++str == end) { - if (blocks > 8) { + if (blocks > max_blocks) { return 0; } goto fixup_ip; @@ -838,35 +840,32 @@ static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) n++; str++; } - if (ip && blocks < 8) { + if (ip && blocks < max_blocks) { ip[blocks] = num; } if (n < 1 || n > 4) { return 0; } - if (++blocks > 8) + if (++blocks > max_blocks) return 0; } fixup_ip: - if (ip && ipv4) { - for (i = 0; i < 5; i++) { - ip[i] = 0; -} - ip[i++] = 0xffff; - ip[i++] = 256 * ip4elm[0] + ip4elm[1]; - ip[i++] = 256 * ip4elm[2] + ip4elm[3]; - } else if (ip && compressed_pos >= 0 && blocks <= 8) { - int offset = 8 - blocks; - for (i = 7; i > compressed_pos + offset; i--) { + if (ip && compressed_pos >= 0 && blocks <= max_blocks) { + int offset = max_blocks - blocks; + for (i = max_blocks - 1; i > compressed_pos + offset; i--) { ip[i] = ip[i - offset]; } for (i = compressed_pos + offset; i >= compressed_pos; i--) { ip[i] = 0; } } + if (ip && ipv4) { + ip[6] = 256 * ip4elm[0] + ip4elm[1]; + ip[7] = 256 * ip4elm[2] + ip4elm[3]; + } - return (compressed_pos >= 0 && blocks <= 8) || blocks == 8; + return (compressed_pos >= 0 && blocks <= max_blocks) || blocks == max_blocks; } /* }}} */ @@ -942,9 +941,14 @@ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0 && ip[6] == 0 && ip[7] == 1) { /* RFC 4291 - Loopback Address */ *reserved = true; - } else if (ip[0] == 0x0064 && ip[1] == 0xff9b) { + } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0) { + /* RFC 4291 - IPv4-Compatible Address (deprecated) */ + *reserved = true; + } else if (ip[0] == 0x0064 && ip[1] == 0xff9b && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0) { /* RFC 6052 - IPv4-IPv6 Translation */ *global = true; + } else if (ip[0] == 0x0064 && ip[1] == 0xff9b && ip[2] == 0x0001) { + /* RFC 8215 - Local-Use IPv4/IPv6 Translation */ } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0xffff) { /* RFC 4291 - IPv4-mapped Address */ *reserved = true; diff --git a/ext/filter/tests/filter_ipv6_dotted_suffix.phpt b/ext/filter/tests/filter_ipv6_dotted_suffix.phpt new file mode 100644 index 000000000000..4fd3d2c03c31 --- /dev/null +++ b/ext/filter/tests/filter_ipv6_dotted_suffix.phpt @@ -0,0 +1,67 @@ +--TEST-- +FILTER_VALIDATE_IP: IPv6 with dotted IPv4 suffix is range-checked like its hexadecimal form +--EXTENSIONS-- +filter +--FILE-- + 'fc00::102:304', + 'fd00::abcd:10.0.0.1' => 'fd00::abcd:a00:1', + '64:ff9b::10.0.0.1' => '64:ff9b::a00:1', + 'fe80::127.0.0.1' => 'fe80::7f00:1', + '2001:db8::1.2.3.4' => '2001:db8::102:304', + '2606:4700::8.8.8.8' => '2606:4700::808:808', + 'fc00:1:2:3:4::1.2.3.4' => 'fc00:1:2:3:4:0:102:304', + '1:2:3:4:5:6:1.2.3.4' => '1:2:3:4:5:6:102:304', + '1::1.2.3.4' => '1::102:304', + '::ffff:1.2.3.4' => '::ffff:102:304', + '::1:1.2.3.4' => '::1:102:304', + '::1.2.3.4' => '::102:304', + '::127.0.0.1' => '::7f00:1', + '::0.0.0.0' => '::', + '::0.0.0.1' => '::1', + '1:2:3:4:5::6:1.2.3.4' => '1:2:3:4:5::6:102:304', + '1:2:3:4:5:6:7:1.2.3.4' => '1:2:3:4:5:6:7:102:304', + ':1.2.3.4' => ':102:304', +]; + +$flags = [ + 'none' => 0, + 'global' => FILTER_FLAG_GLOBAL_RANGE, + 'no_priv' => FILTER_FLAG_NO_PRIV_RANGE, + 'no_res' => FILTER_FLAG_NO_RES_RANGE, +]; + +function check(string $ip, int $flag): string { + return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | $flag) === false ? 'reject' : 'accept'; +} + +foreach ($pairs as $dotted => $hex) { + $out = []; + foreach ($flags as $name => $flag) { + $d = check($dotted, $flag); + $h = check($hex, $flag); + $out[] = "$name=$d" . ($d === $h ? '' : " (hex: $h)"); + } + echo str_pad($dotted, 24), implode(' ', $out), "\n"; +} +?> +--EXPECT-- +fc00::1.2.3.4 none=accept global=reject no_priv=reject no_res=accept +fd00::abcd:10.0.0.1 none=accept global=reject no_priv=reject no_res=accept +64:ff9b::10.0.0.1 none=accept global=accept no_priv=accept no_res=accept +fe80::127.0.0.1 none=accept global=reject no_priv=accept no_res=reject +2001:db8::1.2.3.4 none=accept global=reject no_priv=accept no_res=accept +2606:4700::8.8.8.8 none=accept global=accept no_priv=accept no_res=accept +fc00:1:2:3:4::1.2.3.4 none=accept global=reject no_priv=reject no_res=accept +1:2:3:4:5:6:1.2.3.4 none=accept global=accept no_priv=accept no_res=accept +1::1.2.3.4 none=accept global=accept no_priv=accept no_res=accept +::ffff:1.2.3.4 none=accept global=reject no_priv=accept no_res=reject +::1:1.2.3.4 none=accept global=accept no_priv=accept no_res=accept +::1.2.3.4 none=accept global=reject no_priv=accept no_res=reject +::127.0.0.1 none=accept global=reject no_priv=accept no_res=reject +::0.0.0.0 none=accept global=reject no_priv=accept no_res=reject +::0.0.0.1 none=accept global=reject no_priv=accept no_res=reject +1:2:3:4:5::6:1.2.3.4 none=reject global=reject no_priv=reject no_res=reject +1:2:3:4:5:6:7:1.2.3.4 none=reject global=reject no_priv=reject no_res=reject +:1.2.3.4 none=reject global=reject no_priv=reject no_res=reject diff --git a/ext/filter/tests/filter_ipv6_nat64_local_use.phpt b/ext/filter/tests/filter_ipv6_nat64_local_use.phpt new file mode 100644 index 000000000000..29845b8cc760 --- /dev/null +++ b/ext/filter/tests/filter_ipv6_nat64_local_use.phpt @@ -0,0 +1,73 @@ +--TEST-- +FILTER_FLAG_GLOBAL_RANGE rejects the NAT64 local-use prefix 64:ff9b:1::/48 (RFC 8215) +--EXTENSIONS-- +filter +--FILE-- + +--EXPECT-- +64:ff9b::808:808 + GLOBAL_RANGE: string(16) "64:ff9b::808:808" + NO_PRIV|NO_RES: string(16) "64:ff9b::808:808" +64:ff9b::1 + GLOBAL_RANGE: string(10) "64:ff9b::1" + NO_PRIV|NO_RES: string(10) "64:ff9b::1" +64:ff9b::a9fe:a9fe + GLOBAL_RANGE: string(18) "64:ff9b::a9fe:a9fe" + NO_PRIV|NO_RES: string(18) "64:ff9b::a9fe:a9fe" +64:ff9b::ffff:ffff + GLOBAL_RANGE: string(18) "64:ff9b::ffff:ffff" + NO_PRIV|NO_RES: string(18) "64:ff9b::ffff:ffff" +64:ff9b:1:: + GLOBAL_RANGE: bool(false) + NO_PRIV|NO_RES: string(11) "64:ff9b:1::" +64:ff9b:1::1 + GLOBAL_RANGE: bool(false) + NO_PRIV|NO_RES: string(12) "64:ff9b:1::1" +64:ff9b:1::a9fe:a9fe + GLOBAL_RANGE: bool(false) + NO_PRIV|NO_RES: string(20) "64:ff9b:1::a9fe:a9fe" +64:ff9b:1:ffff:ffff:ffff:ffff:ffff + GLOBAL_RANGE: bool(false) + NO_PRIV|NO_RES: string(34) "64:ff9b:1:ffff:ffff:ffff:ffff:ffff" +64:ff9b:2::1 + GLOBAL_RANGE: string(12) "64:ff9b:2::1" + NO_PRIV|NO_RES: string(12) "64:ff9b:2::1" +64:ff9b:0:1::1 + GLOBAL_RANGE: string(14) "64:ff9b:0:1::1" + NO_PRIV|NO_RES: string(14) "64:ff9b:0:1::1" +64:ff9b::1:0:0 + GLOBAL_RANGE: string(14) "64:ff9b::1:0:0" + NO_PRIV|NO_RES: string(14) "64:ff9b::1:0:0" +64:ff9b:0:0:1:: + GLOBAL_RANGE: string(15) "64:ff9b:0:0:1::" + NO_PRIV|NO_RES: string(15) "64:ff9b:0:0:1::" +64:ff9b:2:: + GLOBAL_RANGE: string(11) "64:ff9b:2::" + NO_PRIV|NO_RES: string(11) "64:ff9b:2::" From b2076de445213e54e1a3cb3ced5f4137e685c5fa Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Thu, 12 Nov 2026 17:03:04 +0100 Subject: [PATCH 2/3] Fix FILTER_VALIDATE_IP range flags ignoring embedded IPv4 addresses 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. GH-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." --- NEWS | 11 ++- ext/filter/logical_filters.c | 34 ++++++--- ext/filter/tests/bug61700.phpt | 2 +- ext/filter/tests/bug77221.phpt | 8 +- .../tests/filter_ipv6_dotted_suffix.phpt | 2 +- .../tests/filter_ipv6_embedded_ipv4.phpt | 73 +++++++++++++++++++ .../tests/filter_ipv6_nat64_local_use.phpt | 26 +++---- 7 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 ext/filter/tests/filter_ipv6_embedded_ipv4.phpt diff --git a/NEWS b/NEWS index f37f1651720b..d31f4f5ca943 100644 --- a/NEWS +++ b/NEWS @@ -50,10 +50,13 @@ PHP NEWS . Fixed stack overflow in filter_var() and filter_var_array() with deeply nested arrays. (Eyüp Can Akman) . Fixed FILTER_VALIDATE_IP range flags checking IPv6 addresses with a - dotted IPv4 suffix (e.g. fc00::1.2.3.4) as ::ffff:a.b.c.d. The deprecated - IPv4-compatible block ::/96 is now reserved. (Xavier Leune) - . Fixed FILTER_FLAG_GLOBAL_RANGE accepting the NAT64 local-use prefix - 64:ff9b:1::/48. (Xavier Leune) + dotted IPv4 suffix (e.g. fc00::1.2.3.4) as ::ffff:a.b.c.d. (Xavier Leune) + . Fixed FILTER_VALIDATE_IP range flags treating the NAT64 local-use prefix + 64:ff9b:1::/48 as global. It is now private. (Xavier Leune) + . Fixed FILTER_VALIDATE_IP range flags ignoring the IPv4 address embedded + in NAT64 (64:ff9b::/96), 6to4 (still not global), IPv4-mapped, + IPv4-compatible (::/96, now always reserved) and IPv4-translated IPv6 + addresses. (Xavier Leune) - MBString: . Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c index b73e8be5253e..9955bce4d305 100644 --- a/ext/filter/logical_filters.c +++ b/ext/filter/logical_filters.c @@ -928,7 +928,19 @@ static bool ipv4_get_status_flags(const int ip[8], bool *global, bool *reserved, return true; } -/* From the tables in RFC 6890 - Special-Purpose IP Address Registries */ +/* Flags of an IPv4 address embedded in an IPv6 address as two 16-bit blocks */ +static void ipv6_get_embedded_ipv4_status_flags(int high, int low, bool *global, bool *reserved, bool *private) +{ + const int ip4[8] = { high >> 8, high & 0xff, low >> 8, low & 0xff }; + + if (!ipv4_get_status_flags(ip4, global, reserved, private)) { + /* not a special-purpose IPv4 address */ + *global = true; + } +} + +/* From the tables in RFC 6890 - Special-Purpose IP Address Registries, plus a few later blocks. + * Also treats addresses embedding an IPv4 address according to that address. */ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, bool *private) { *global = false; @@ -941,17 +953,19 @@ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0 && ip[6] == 0 && ip[7] == 1) { /* RFC 4291 - Loopback Address */ *reserved = true; - } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0) { - /* RFC 4291 - IPv4-Compatible Address (deprecated) */ + } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 + && ((ip[4] == 0 && (ip[5] == 0 || ip[5] == 0xffff)) || (ip[4] == 0xffff && ip[5] == 0))) { + /* RFC 4291 - IPv4-Compatible (deprecated) and IPv4-mapped Address, RFC 2765 - IPv4-translated Address: + * reserved, and private if the embedded IPv4 address is */ + ipv6_get_embedded_ipv4_status_flags(ip[6], ip[7], global, reserved, private); + *global = false; *reserved = true; } else if (ip[0] == 0x0064 && ip[1] == 0xff9b && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0) { - /* RFC 6052 - IPv4-IPv6 Translation */ - *global = true; + /* RFC 6052 - IPv4-IPv6 Translation, classified as the embedded IPv4 address */ + ipv6_get_embedded_ipv4_status_flags(ip[6], ip[7], global, reserved, private); } else if (ip[0] == 0x0064 && ip[1] == 0xff9b && ip[2] == 0x0001) { /* RFC 8215 - Local-Use IPv4/IPv6 Translation */ - } else if (ip[0] == 0 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0 && ip[4] == 0 && ip[5] == 0xffff) { - /* RFC 4291 - IPv4-mapped Address */ - *reserved = true; + *private = true; } else if (ip[0] == 0x0100 && ip[1] == 0 && ip[2] == 0 && ip[3] == 0) { /* RFC 6666 - Discard-Only Address Block */ } else if (ip[0] == 0x2001 && ip[1] == 0x0000) { @@ -965,7 +979,9 @@ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, } else if (ip[0] == 0x2001 && ip[1] >= 0x0010 && ip[1] <= 0x001f) { /* RFC 4843 - ORCHID */ } else if (ip[0] == 0x2002) { - /* RFC 3056 - 6to4 */ + /* RFC 3056 - 6to4, not global (RFC 7526), private or reserved if the embedded IPv4 address is */ + ipv6_get_embedded_ipv4_status_flags(ip[1], ip[2], global, reserved, private); + *global = false; } else if (ip[0] >= 0xfc00 && ip[0] <= 0xfdff) { /* RFC 4193 - Unique-Local */ *private = true; diff --git a/ext/filter/tests/bug61700.phpt b/ext/filter/tests/bug61700.phpt index a571b329b5aa..1636ccaf796a 100644 --- a/ext/filter/tests/bug61700.phpt +++ b/ext/filter/tests/bug61700.phpt @@ -10,5 +10,5 @@ var_dump(filter_var('0:0:0:0:0:0:0:1', FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RA ?> --EXPECT-- bool(false) -string(18) "::ffff:192.168.1.1" +bool(false) bool(false) diff --git a/ext/filter/tests/bug77221.phpt b/ext/filter/tests/bug77221.phpt index f94a14dee2de..6c996fe7fb19 100644 --- a/ext/filter/tests/bug77221.phpt +++ b/ext/filter/tests/bug77221.phpt @@ -42,6 +42,8 @@ foreach ($non_global_ranges as $key => $range) { var_dump(filter_var('185.85.0.29', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); var_dump(filter_var('64:ff9b::', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); var_dump(filter_var('64:ff9b::ffff', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); +var_dump(filter_var('64:ff9b::808:808', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); +var_dump(filter_var('2606:4700::1', FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE)); ?> --EXPECT-- @@ -115,5 +117,7 @@ string(9) "fe80::/10" bool(false) bool(false) string(11) "185.85.0.29" -string(9) "64:ff9b::" -string(13) "64:ff9b::ffff" +bool(false) +bool(false) +string(16) "64:ff9b::808:808" +string(12) "2606:4700::1" diff --git a/ext/filter/tests/filter_ipv6_dotted_suffix.phpt b/ext/filter/tests/filter_ipv6_dotted_suffix.phpt index 4fd3d2c03c31..979a08fd4870 100644 --- a/ext/filter/tests/filter_ipv6_dotted_suffix.phpt +++ b/ext/filter/tests/filter_ipv6_dotted_suffix.phpt @@ -49,7 +49,7 @@ foreach ($pairs as $dotted => $hex) { --EXPECT-- fc00::1.2.3.4 none=accept global=reject no_priv=reject no_res=accept fd00::abcd:10.0.0.1 none=accept global=reject no_priv=reject no_res=accept -64:ff9b::10.0.0.1 none=accept global=accept no_priv=accept no_res=accept +64:ff9b::10.0.0.1 none=accept global=reject no_priv=reject no_res=accept fe80::127.0.0.1 none=accept global=reject no_priv=accept no_res=reject 2001:db8::1.2.3.4 none=accept global=reject no_priv=accept no_res=accept 2606:4700::8.8.8.8 none=accept global=accept no_priv=accept no_res=accept diff --git a/ext/filter/tests/filter_ipv6_embedded_ipv4.phpt b/ext/filter/tests/filter_ipv6_embedded_ipv4.phpt new file mode 100644 index 000000000000..7bd4e7e02cdb --- /dev/null +++ b/ext/filter/tests/filter_ipv6_embedded_ipv4.phpt @@ -0,0 +1,73 @@ +--TEST-- +FILTER_VALIDATE_IP: range flags take the IPv4 address embedded in an IPv6 address into account +--EXTENSIONS-- +filter +--FILE-- + FILTER_FLAG_GLOBAL_RANGE, 'no_priv' => FILTER_FLAG_NO_PRIV_RANGE, 'no_res' => FILTER_FLAG_NO_RES_RANGE] as $name => $flag) { + $out[] = $name . '=' . (filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | $flag) === false ? 'reject' : 'accept'); + } + echo str_pad($address, 26), implode(' ', $out), "\n"; +} +?> +--EXPECT-- +64:ff9b::8.8.8.8 global=accept no_priv=accept no_res=accept +64:ff9b::808:808 global=accept no_priv=accept no_res=accept +64:ff9b::10.0.0.1 global=reject no_priv=reject no_res=accept +64:ff9b::a00:1 global=reject no_priv=reject no_res=accept +64:ff9b::127.0.0.1 global=reject no_priv=accept no_res=reject +64:ff9b::169.254.169.254 global=reject no_priv=accept no_res=reject +64:ff9b::100.64.0.1 global=reject no_priv=accept no_res=accept +64:FF9B::0808:0808 global=accept no_priv=accept no_res=accept +0064:ff9b:0:0:0:0:808:808 global=accept no_priv=accept no_res=accept +64:ff9b:0:0:0:0:a9fe:a9fe global=reject no_priv=accept no_res=reject +64:ff9b:: global=reject no_priv=accept no_res=reject +64:ff9b::c058:6301 global=accept no_priv=accept no_res=accept +::ffff:8.8.8.8 global=reject no_priv=accept no_res=reject +::ffff:10.0.0.1 global=reject no_priv=reject no_res=reject +::FFFF:0A00:0001 global=reject no_priv=reject no_res=reject +::ffff:c0a8:101 global=reject no_priv=reject no_res=reject +::ffff:127.0.0.1 global=reject no_priv=accept no_res=reject +::8.8.8.8 global=reject no_priv=accept no_res=reject +::808:808 global=reject no_priv=accept no_res=reject +::10.0.0.1 global=reject no_priv=reject no_res=reject +::127.0.0.1 global=reject no_priv=accept no_res=reject +::2 global=reject no_priv=accept no_res=reject +::ffff:0:8.8.8.8 global=reject no_priv=accept no_res=reject +::ffff:0:10.0.0.1 global=reject no_priv=reject no_res=reject +::ffff:0:7f00:1 global=reject no_priv=accept no_res=reject +2002:808:808::1 global=reject no_priv=accept no_res=accept +2002:a00:1:: global=reject no_priv=reject no_res=accept +2002:c0a8:101::1 global=reject no_priv=reject no_res=accept +2002:7f00:1:: global=reject no_priv=accept no_res=reject +2002:a9fe:a9fe::1 global=reject no_priv=accept no_res=reject +2002:6440:1:: global=reject no_priv=accept no_res=accept +64:ff9b:1::808:808 global=reject no_priv=reject no_res=accept +64:ff9b:1:a00:1:: global=reject no_priv=reject no_res=accept +64:ff9b:1::10.0.0.1 global=reject no_priv=reject no_res=accept +:: global=reject no_priv=accept no_res=reject +::1 global=reject no_priv=accept no_res=reject diff --git a/ext/filter/tests/filter_ipv6_nat64_local_use.phpt b/ext/filter/tests/filter_ipv6_nat64_local_use.phpt index 29845b8cc760..ace5d65d9193 100644 --- a/ext/filter/tests/filter_ipv6_nat64_local_use.phpt +++ b/ext/filter/tests/filter_ipv6_nat64_local_use.phpt @@ -1,16 +1,16 @@ --TEST-- -FILTER_FLAG_GLOBAL_RANGE rejects the NAT64 local-use prefix 64:ff9b:1::/48 (RFC 8215) +FILTER_VALIDATE_IP: the NAT64 local-use prefix 64:ff9b:1::/48 (RFC 8215) is private --EXTENSIONS-- filter --FILE-- Date: Thu, 12 Nov 2026 17:03:27 +0100 Subject: [PATCH 3/3] Fix FILTER_VALIDATE_IP range flags missing special-purpose blocks 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. --- NEWS | 3 ++ ext/filter/logical_filters.c | 21 +++++++-- .../tests/filter_ip_special_blocks.phpt | 45 +++++++++++++++++++ ext/filter/tests/filter_ipv4_rfc6890.phpt | 10 ++++- 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 ext/filter/tests/filter_ip_special_blocks.phpt diff --git a/NEWS b/NEWS index d31f4f5ca943..afd696665157 100644 --- a/NEWS +++ b/NEWS @@ -57,6 +57,9 @@ PHP NEWS in NAT64 (64:ff9b::/96), 6to4 (still not global), IPv4-mapped, IPv4-compatible (::/96, now always reserved) and IPv4-translated IPv6 addresses. (Xavier Leune) + . Fixed FILTER_VALIDATE_IP range flags missing the IPv6 site-local + (fec0::/10, private), multicast (ff00::/8 and 224.0.0.0/4, reserved), + documentation (3fff::/20) and SRv6 (5f00::/16) blocks. (Xavier Leune) - MBString: . Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c index 9955bce4d305..7be1f11f4238 100644 --- a/ext/filter/logical_filters.c +++ b/ext/filter/logical_filters.c @@ -869,8 +869,9 @@ static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) } /* }}} */ -/* From the tables in RFC 6890 - Special-Purpose IP Address Registriesi - * Including errata: https://www.rfc-editor.org/errata_search.php?rfc=6890&rec_status=1 */ +/* From the tables in RFC 6890 - Special-Purpose IP Address Registries + * Including errata: https://www.rfc-editor.org/errata_search.php?rfc=6890&rec_status=1 + * Also treats multicast as reserved, as it is not a unicast destination */ static bool ipv4_get_status_flags(const int ip[8], bool *global, bool *reserved, bool *private) { *global = false; @@ -915,6 +916,9 @@ static bool ipv4_get_status_flags(const int ip[8], bool *global, bool *reserved, /* RFC 5737 - Documentation */ } else if (ip[0] == 203 && ip[1] == 0 && ip[2] == 113) { /* RFC 5737 - Documentation */ + } else if (ip[0] >= 224 && ip[0] <= 239) { + /* RFC 5771 - Multicast */ + *reserved = true; } else if (ip[0] >= 240 && ip[1] <= 255) { /* RFC 1122 - Reserved */ *reserved = true; @@ -940,7 +944,8 @@ static void ipv6_get_embedded_ipv4_status_flags(int high, int low, bool *global, } /* From the tables in RFC 6890 - Special-Purpose IP Address Registries, plus a few later blocks. - * Also treats addresses embedding an IPv4 address according to that address. */ + * Also treats multicast as reserved, deprecated site-local addresses as private, and addresses + * embedding an IPv4 address according to that address. */ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, bool *private) { *global = false; @@ -988,6 +993,16 @@ static bool ipv6_get_status_flags(const int ip[8], bool *global, bool *reserved, } else if (ip[0] >= 0xfe80 && ip[0] <= 0xfebf) { /* RFC 4291 - Linked-Scoped Unicast */ *reserved = true; + } else if (ip[0] >= 0xfec0 && ip[0] <= 0xfeff) { + /* RFC 3879 - Site-Local Unicast (deprecated) */ + *private = true; + } else if (ip[0] >= 0xff00) { + /* RFC 4291 - Multicast */ + *reserved = true; + } else if (ip[0] == 0x3fff && ip[1] <= 0x0fff) { + /* RFC 9637 - Documentation */ + } else if (ip[0] == 0x5f00) { + /* RFC 9602 - Segment Routing (SRv6) SIDs */ } else { return false; } diff --git a/ext/filter/tests/filter_ip_special_blocks.phpt b/ext/filter/tests/filter_ip_special_blocks.phpt new file mode 100644 index 000000000000..ef3afdc5af89 --- /dev/null +++ b/ext/filter/tests/filter_ip_special_blocks.phpt @@ -0,0 +1,45 @@ +--TEST-- +FILTER_VALIDATE_IP: site-local, multicast, documentation (3fff::/20) and SRv6 blocks +--EXTENSIONS-- +filter +--FILE-- + FILTER_FLAG_GLOBAL_RANGE, 'no_priv' => FILTER_FLAG_NO_PRIV_RANGE, 'no_res' => FILTER_FLAG_NO_RES_RANGE] as $name => $flag) { + $out[] = $name . '=' . (filter_var($address, FILTER_VALIDATE_IP, $flag) === false ? 'reject' : 'accept'); + } + echo str_pad($address, 18), implode(' ', $out), "\n"; +} +?> +--EXPECT-- +fec0::1 global=reject no_priv=reject no_res=accept +feff:ffff::1 global=reject no_priv=reject no_res=accept +ff02::1 global=reject no_priv=accept no_res=reject +ff05::1:3 global=reject no_priv=accept no_res=reject +ffff::1 global=reject no_priv=accept no_res=reject +3fff::1 global=reject no_priv=accept no_res=accept +3fff:fff::1 global=reject no_priv=accept no_res=accept +3fff:1000::1 global=accept no_priv=accept no_res=accept +5f00::1 global=reject no_priv=accept no_res=accept +5f01::1 global=accept no_priv=accept no_res=accept +223.255.255.255 global=accept no_priv=accept no_res=accept +224.0.0.0 global=reject no_priv=accept no_res=reject +239.255.255.255 global=reject no_priv=accept no_res=reject +fe80::1 global=reject no_priv=accept no_res=reject +fdff::1 global=reject no_priv=reject no_res=accept diff --git a/ext/filter/tests/filter_ipv4_rfc6890.phpt b/ext/filter/tests/filter_ipv4_rfc6890.phpt index 2ae55c60787c..240d1f03c0a0 100644 --- a/ext/filter/tests/filter_ipv4_rfc6890.phpt +++ b/ext/filter/tests/filter_ipv4_rfc6890.phpt @@ -70,8 +70,11 @@ $reservedRanges['192.168.0.0/16'] = array('192.168.0.0', '192.168.255.255'); // 203.0.113.0/24 $reservedRanges['203.0.113.0/24'] = array('203.0.113.0', '203.0.113.255'); +// 224.0.0.0/4 +$reservedRanges['224.0.0.0/4'] = array('224.0.0.0', '239.255.255.255'); + // 240.0.0.0/4 + 255.255.255.255/32 -$reservedRanges['240.0.0.0/4'] = array('224.0.0.0', '255.255.255.255'); +$reservedRanges['240.0.0.0/4'] = array('240.0.0.0', '255.255.255.255'); foreach ($reservedRanges as $key => $range) { list($min, $max) = $range; @@ -136,6 +139,9 @@ string(15) "192.168.255.255" string(14) "203.0.113.0/24" string(11) "203.0.113.0" string(13) "203.0.113.255" +string(11) "224.0.0.0/4" +bool(false) +bool(false) string(11) "240.0.0.0/4" -string(9) "224.0.0.0" +bool(false) bool(false)