Skip to content
Open
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
11 changes: 11 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ 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. (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)
. 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
Expand Down
85 changes: 60 additions & 25 deletions ext/filter/logical_filters.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -838,40 +840,38 @@ 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;
}
/* }}} */

/* 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;
Expand Down Expand Up @@ -916,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;
Expand All @@ -929,7 +932,20 @@ 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 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;
Expand All @@ -942,12 +958,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] == 0x0064 && ip[1] == 0xff9b) {
/* RFC 6052 - IPv4-IPv6 Translation */
*global = true;
} 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 */
} 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, 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 */
*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) {
Expand All @@ -961,13 +984,25 @@ 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;
} 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;
}
Expand Down
2 changes: 1 addition & 1 deletion ext/filter/tests/bug61700.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
8 changes: 6 additions & 2 deletions ext/filter/tests/bug77221.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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--
Expand Down Expand Up @@ -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"
45 changes: 45 additions & 0 deletions ext/filter/tests/filter_ip_special_blocks.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
FILTER_VALIDATE_IP: site-local, multicast, documentation (3fff::/20) and SRv6 blocks
--EXTENSIONS--
filter
--FILE--
<?php
$addresses = [
// RFC 3879 - Site-Local Unicast fec0::/10 (deprecated): private
'fec0::1', 'feff:ffff::1',
// RFC 4291 - Multicast ff00::/8: reserved
'ff02::1', 'ff05::1:3', 'ffff::1',
// RFC 9637 - Documentation 3fff::/20: not global
'3fff::1', '3fff:fff::1', '3fff:1000::1',
// RFC 9602 - SRv6 SIDs 5f00::/16: not global
'5f00::1', '5f01::1',
// RFC 5771 - IPv4 Multicast 224.0.0.0/4: reserved
'223.255.255.255', '224.0.0.0', '239.255.255.255',
// Neighbours, unchanged
'fe80::1', 'fdff::1',
];

foreach ($addresses as $address) {
$out = [];
foreach (['global' => 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
10 changes: 8 additions & 2 deletions ext/filter/tests/filter_ipv4_rfc6890.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
67 changes: 67 additions & 0 deletions ext/filter/tests/filter_ipv6_dotted_suffix.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
FILTER_VALIDATE_IP: IPv6 with dotted IPv4 suffix is range-checked like its hexadecimal form
--EXTENSIONS--
filter
--FILE--
<?php
$pairs = [
'fc00::1.2.3.4' => '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=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
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
Loading
Loading