mirror of
https://github.com/webmin/webmin.git
synced 2026-06-04 20:30:22 +01:00
Fix check_ip6address in web-lib-funcs
This commit is contained in:
@@ -35,9 +35,9 @@ subtest 'check_ipaddress' => sub {
|
||||
|
||||
# check_ip6address — IPv6, optionally with /N netmask suffix.
|
||||
#
|
||||
# Unlike the docstring, this sub also accepts an address/netmask form, but
|
||||
# only when the `::` shorthand is at the *start* of the address — see the
|
||||
# bug notes below. Pin current behaviour so a future fix shows up loudly.
|
||||
# Accepts the standard text forms, the "::" shorthand at any position, an
|
||||
# optional /N netmask, and the IPv4-in-IPv6 dotted-quad tail (RFC 4291
|
||||
# §2.5.5: "::ffff:N.N.N.N" mapped and "X:X:X:X:X:X:N.N.N.N" compatible).
|
||||
subtest 'check_ip6address' => sub {
|
||||
ok( main::check_ip6address('::'), 'unspecified accepted');
|
||||
ok( main::check_ip6address('::1'), 'loopback accepted');
|
||||
@@ -45,23 +45,27 @@ subtest 'check_ip6address' => sub {
|
||||
ok( main::check_ip6address('1:2:3:4:5:6:7:8'), 'full eight-block form accepted');
|
||||
ok( main::check_ip6address('2001:db8::'), 'trailing :: accepted (no netmask)');
|
||||
|
||||
# Netmask suffix.
|
||||
ok( main::check_ip6address('::1/64'), 'address/netmask accepted when :: is at start');
|
||||
ok(!main::check_ip6address('::1/200'), 'netmask > 128 rejected');
|
||||
# Netmask suffix — both with leading and trailing :: shorthand.
|
||||
ok( main::check_ip6address('::1/64'), 'address/netmask accepted with leading ::');
|
||||
ok( main::check_ip6address('2001:db8::/32'), 'address/netmask accepted with trailing ::');
|
||||
ok( main::check_ip6address('::/0'), '::/0 default route accepted');
|
||||
ok( main::check_ip6address('fe80::/10'), 'fe80::/10 link-local prefix accepted');
|
||||
ok(!main::check_ip6address('::1/200'), 'netmask > 128 rejected');
|
||||
|
||||
# BUG: a netmask suffix combined with a trailing `::` shorthand
|
||||
# fails. The validator's empty-block accounting is thrown off because
|
||||
# split() no longer trims trailing empties when the final element is
|
||||
# the netmask. Real-world example: "2001:db8::/32" — a perfectly
|
||||
# valid CIDR — is rejected.
|
||||
ok(!main::check_ip6address('2001:db8::/32'),
|
||||
'BUG: valid CIDR with trailing :: rejected by validator');
|
||||
# IPv4-in-IPv6 tails.
|
||||
ok( main::check_ip6address('::ffff:10.0.0.1'), 'IPv4-mapped (::ffff:N.N.N.N) accepted');
|
||||
ok( main::check_ip6address('::ffff:0.0.0.0'), 'IPv4-mapped all-zero accepted');
|
||||
ok( main::check_ip6address('::1.2.3.4'), 'IPv4-compatible (::N.N.N.N) accepted');
|
||||
ok( main::check_ip6address('0:0:0:0:0:ffff:1.2.3.4'),
|
||||
'fully-expanded IPv4-mapped accepted');
|
||||
ok(!main::check_ip6address('::ffff:256.0.0.1'), 'IPv4-mapped with octet > 255 rejected');
|
||||
ok(!main::check_ip6address('::ffff:1.2.3'), 'IPv4-mapped with too-few octets rejected');
|
||||
|
||||
# BUG: IPv4-mapped IPv6 (RFC 4291 §2.5.5.2) is rejected because the
|
||||
# per-block regex requires hex digits. Notably, is_non_public_ipaddress
|
||||
# has an unreachable ::ffff:N.N.N.N branch downstream of this check.
|
||||
ok(!main::check_ip6address('::ffff:10.0.0.1'),
|
||||
'BUG: IPv4-mapped IPv6 rejected by validator');
|
||||
# Bare IPv4 must be rejected — callers (e.g. ip_match) use this sub
|
||||
# as a type discriminator and a true result re-routes IPv4 input
|
||||
# through the IPv6 codepath.
|
||||
ok(!main::check_ip6address('10.0.0.1'), 'bare IPv4 rejected (type-discriminator contract)');
|
||||
ok(!main::check_ip6address('1.2.3.4'), 'bare IPv4 rejected (type-discriminator contract)');
|
||||
|
||||
ok(!main::check_ip6address('gggg::1'), 'non-hex rejected');
|
||||
ok(!main::check_ip6address('1:2:3:4:5:6:7:8:9'), 'too many groups rejected');
|
||||
@@ -111,12 +115,11 @@ subtest 'is_non_public_ipaddress (IPv6)' => sub {
|
||||
ok( main::is_non_public_ipaddress('fc00::1'), 'ULA (fc00)');
|
||||
ok( main::is_non_public_ipaddress('fd12::1'), 'ULA (fd12)');
|
||||
|
||||
# IPv4-mapped (::ffff:N.N.N.N) is meant to recurse on the embedded
|
||||
# IPv4, but the branch is unreachable: check_ip6address rejects all
|
||||
# ::ffff:N.N.N.N inputs (see BUG note in check_ip6address subtest).
|
||||
# Both calls below currently return 0 — pin that.
|
||||
ok(!main::is_non_public_ipaddress('::ffff:10.0.0.1'),
|
||||
'BUG: ::ffff:<private> falsely reported as public (validator rejects input)');
|
||||
# IPv4-mapped (::ffff:N.N.N.N) recurses on the embedded IPv4.
|
||||
ok( main::is_non_public_ipaddress('::ffff:10.0.0.1'),
|
||||
'::ffff:<private> recurses → non-public');
|
||||
ok( main::is_non_public_ipaddress('::ffff:192.168.1.1'),
|
||||
'::ffff:<rfc1918> recurses → non-public');
|
||||
ok(!main::is_non_public_ipaddress('::ffff:8.8.8.8'),
|
||||
'::ffff:<public> reported as public');
|
||||
|
||||
|
||||
@@ -693,35 +693,48 @@ Check if some IPv6 address is properly formatted, and returns 1 if so.
|
||||
=cut
|
||||
sub check_ip6address
|
||||
{
|
||||
# Special case for unspecified address (analogous to 0.0.0.0 in IPv4)
|
||||
return 1 if ($_[0] eq "::");
|
||||
my @blocks = split(/:/, $_[0]);
|
||||
return 0 if (@blocks == 0 || @blocks > 8);
|
||||
|
||||
# The address/netmask format is accepted. So we're looking for a "/" to isolate a possible netmask.
|
||||
# After that, we delete the netmask to control the address only format, but we verify whether the netmask
|
||||
# value is in [0;128].
|
||||
my $ib = $#blocks;
|
||||
my $where = index($blocks[$ib],"/");
|
||||
my $addr = $_[0];
|
||||
my $m = 0;
|
||||
if ($where != -1) {
|
||||
my $b = substr($blocks[$ib],0,$where);
|
||||
$m = substr($blocks[$ib],$where+1,length($blocks[$ib])-($where+1));
|
||||
$blocks[$ib]=$b;
|
||||
}
|
||||
|
||||
# The netmask must take its value in [0;128]
|
||||
return 0 if ($m <0 || $m >128);
|
||||
# Strip an optional /N netmask before splitting. Doing this on the
|
||||
# raw string (rather than from the last split element) keeps split()'s
|
||||
# trailing-empty accounting intact for inputs like "2001:db8::/32",
|
||||
# where the netmask would otherwise hide the trailing "::" shorthand.
|
||||
if ($addr =~ s{/(\d+)\z}{}) {
|
||||
$m = $1;
|
||||
}
|
||||
return 0 if ($m < 0 || $m > 128);
|
||||
|
||||
# Special case for unspecified address (analogous to 0.0.0.0 in IPv4),
|
||||
# both bare and with a netmask.
|
||||
return 1 if ($addr eq "::");
|
||||
|
||||
my @blocks = split(/:/, $addr);
|
||||
return 0 if (@blocks == 0);
|
||||
|
||||
# Accept the IPv4-in-IPv6 forms (RFC 4291 §2.5.5: "::ffff:N.N.N.N"
|
||||
# IPv4-mapped, and the more general "X:X:X:X:X:X:N.N.N.N"). If the
|
||||
# last block is a dotted-quad, validate the octets and count it as two
|
||||
# 16-bit groups for the overall 8-group ceiling. The leading ":" guard
|
||||
# distinguishes IPv4-tailed IPv6 from a bare IPv4 address — callers
|
||||
# like ip_match() rely on this sub returning false for "10.0.0.1".
|
||||
my $count = scalar(@blocks);
|
||||
if ($addr =~ /:/ &&
|
||||
$blocks[-1] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\z/) {
|
||||
return 0 if ($1 > 255 || $2 > 255 || $3 > 255 || $4 > 255);
|
||||
$count++;
|
||||
pop(@blocks);
|
||||
}
|
||||
return 0 if ($count > 8);
|
||||
|
||||
# Check the different blocks of the address : 16 bits block in hexa notation.
|
||||
# Possibility of 1 empty block or 2 if the address begins with "::".
|
||||
my $b;
|
||||
my $empty = 0;
|
||||
foreach $b (@blocks) {
|
||||
foreach my $b (@blocks) {
|
||||
return 0 if ($b ne "" && $b !~ /^[0-9a-f]{1,4}$/i);
|
||||
$empty++ if ($b eq "");
|
||||
}
|
||||
return 0 if ($empty > 1 && !($_[0] =~ /^::/ && $empty == 2));
|
||||
return 0 if ($empty > 1 && !($addr =~ /^::/ && $empty == 2));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user