Fix check_ip6address in miniserv

This commit is contained in:
Joe Cooper
2026-05-20 14:04:53 -05:00
parent d2ba0d910b
commit 3e38e3268e
2 changed files with 70 additions and 19 deletions

View File

@@ -7027,26 +7027,49 @@ return $_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ &&
}
# Check if some IPv6 address is properly formatted, and returns 1 if so.
# Kept in sync with web-lib-funcs.pl's copy.
sub check_ip6address
{
my @blocks = split(/:/, $_[0]);
return 0 if (@blocks == 0 || @blocks > 8);
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;
}
return 0 if ($m <0 || $m >128);
my $b;
# 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);
my $empty = 0;
foreach $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));
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 && !($addr =~ /^::/ && $empty == 2));
return 1;
}

View File

@@ -264,13 +264,41 @@ subtest 'check_ipaddress' => sub {
ok(!miniserv::check_ipaddress('not an ip'), 'garbage rejected');
};
# Kept in lockstep with t/web-lib-funcs-ip.t's matching subtest, since the
# two copies of check_ip6address must accept and reject the same inputs.
subtest 'check_ip6address' => sub {
ok( miniserv::check_ip6address('::'), 'unspecified accepted');
ok( miniserv::check_ip6address('::1'), 'loopback accepted');
ok( miniserv::check_ip6address('2001:db8::1'), 'compressed form accepted');
ok( miniserv::check_ip6address('1:2:3:4:5:6:7:8'), 'full form accepted');
ok(!miniserv::check_ip6address('not an addr'), 'garbage rejected');
ok(!miniserv::check_ip6address('1:2:3:4:5:6:7:8:9'), 'too many groups rejected');
ok( miniserv::check_ip6address('1:2:3:4:5:6:7:8'), 'full eight-block form accepted');
ok( miniserv::check_ip6address('2001:db8::'), 'trailing :: accepted (no netmask)');
# Netmask suffix — both with leading and trailing :: shorthand.
ok( miniserv::check_ip6address('::1/64'), 'address/netmask accepted with leading ::');
ok( miniserv::check_ip6address('2001:db8::/32'), 'address/netmask accepted with trailing ::');
ok( miniserv::check_ip6address('::/0'), '::/0 default route accepted');
ok( miniserv::check_ip6address('fe80::/10'), 'fe80::/10 link-local prefix accepted');
ok(!miniserv::check_ip6address('::1/200'), 'netmask > 128 rejected');
# IPv4-in-IPv6 tails.
ok( miniserv::check_ip6address('::ffff:10.0.0.1'), 'IPv4-mapped (::ffff:N.N.N.N) accepted');
ok( miniserv::check_ip6address('::ffff:0.0.0.0'), 'IPv4-mapped all-zero accepted');
ok( miniserv::check_ip6address('::1.2.3.4'), 'IPv4-compatible (::N.N.N.N) accepted');
ok( miniserv::check_ip6address('0:0:0:0:0:ffff:1.2.3.4'),
'fully-expanded IPv4-mapped accepted');
ok(!miniserv::check_ip6address('::ffff:256.0.0.1'), 'IPv4-mapped with octet > 255 rejected');
ok(!miniserv::check_ip6address('::ffff:1.2.3'), 'IPv4-mapped with too-few octets rejected');
# 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(!miniserv::check_ip6address('10.0.0.1'), 'bare IPv4 rejected (type-discriminator contract)');
ok(!miniserv::check_ip6address('1.2.3.4'), 'bare IPv4 rejected (type-discriminator contract)');
ok(!miniserv::check_ip6address('gggg::1'), 'non-hex rejected');
ok(!miniserv::check_ip6address('1:2:3:4:5:6:7:8:9'), 'too many groups rejected');
ok(!miniserv::check_ip6address('::1::2'), 'multiple :: rejected');
ok(!miniserv::check_ip6address('not an addr'), 'garbage rejected');
};
# canonicalize_ip6 / expand_ipv6_bytes