mirror of
https://github.com/webmin/webmin.git
synced 2026-06-22 12:10:28 +01:00
Fix IPv6 CIDR access control matching
This PR fixes Webmin IP access control handling for IPv6 CIDR prefixes that are not divisible by 8, such as `/29` as mentioned in this https://github.com/webmin/webmin/issues/1570 ticket. Before Webmin validation rejected non-byte-aligned IPv6 network sizes, and the runtime matcher compared IPv6 networks only by whole bytes. This meant valid IPv6 CIDR prefixes could not be used safely in access control rules. Changes: - Allow IPv6 access-control prefixes from `/0` through `/128`, without requiring divisibility by 8. - Add bit-accurate IPv6 prefix matching for ACL checks. - Apply the same matching behavior in both `miniserv.pl` and `webmin/webmin-lib.pl`. - Fix IPv6 canonicalization for `::` and trailing `::` forms used by the matcher. - Add regression tests for `/0`, `/29`, `/32`, `/63`, `/64`, `/127`, and `/128`.
This commit is contained in:
41
miniserv.pl
41
miniserv.pl
@@ -3200,14 +3200,7 @@ for($i=2; $i<@_; $i++) {
|
||||
# Compare with an IPv6 network
|
||||
local $v6size = $2;
|
||||
local $v6addr = &canonicalize_ip6($1);
|
||||
local $bytes = $v6size / 8;
|
||||
@mo = &expand_ipv6_bytes($v6addr);
|
||||
local @io6 = &expand_ipv6_bytes(&canonicalize_ip6($_[0]));
|
||||
for($j=0; $j<$bytes; $j++) {
|
||||
if ($mo[$j] ne $io6[$j]) {
|
||||
$mismatch = 1;
|
||||
}
|
||||
}
|
||||
$mismatch = 1 if (!&ip6_prefix_match($_[0], $v6addr, $v6size));
|
||||
}
|
||||
elsif ($_[$i] !~ /^[0-9\.]+$/) {
|
||||
# Compare with hostname
|
||||
@@ -7284,16 +7277,20 @@ sub canonicalize_ip6
|
||||
{
|
||||
my ($addr) = @_;
|
||||
return $addr if (!&check_ip6address($addr));
|
||||
my @w = split(/:/, $addr);
|
||||
my @w = split(/:/, $addr, -1);
|
||||
my $idx = &indexof("", @w);
|
||||
if ($idx >= 0) {
|
||||
# Expand ::
|
||||
my $mis = 8 - scalar(@w);
|
||||
my @nw = @w[0..$idx];
|
||||
my $endidx = $idx;
|
||||
while($endidx+1 < @w && $w[$endidx+1] eq "") {
|
||||
$endidx++;
|
||||
}
|
||||
my $mis = 8 - (scalar(@w) - ($endidx - $idx + 1));
|
||||
my @nw = @w[0..$idx-1];
|
||||
for(my $i=0; $i<$mis; $i++) {
|
||||
push(@nw, 0);
|
||||
}
|
||||
push(@nw, @w[$idx+1 .. $#w]);
|
||||
push(@nw, @w[$endidx+1 .. $#w]);
|
||||
@w = @nw;
|
||||
}
|
||||
foreach my $w (@w) {
|
||||
@@ -7317,6 +7314,26 @@ foreach my $w (split(/:/, $addr)) {
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# ip6_prefix_match(address, network, prefix)
|
||||
# Returns 1 if an IPv6 address is in a network prefix.
|
||||
sub ip6_prefix_match
|
||||
{
|
||||
my ($addr, $network, $prefix) = @_;
|
||||
my @addr = &expand_ipv6_bytes(&canonicalize_ip6($addr));
|
||||
my @network = &expand_ipv6_bytes(&canonicalize_ip6($network));
|
||||
return 0 if (@addr != 16 || @network != 16 || $prefix < 0 || $prefix > 128);
|
||||
my $bytes = int($prefix / 8);
|
||||
my $bits = $prefix % 8;
|
||||
for(my $i=0; $i<$bytes; $i++) {
|
||||
return 0 if ($addr[$i] != $network[$i]);
|
||||
}
|
||||
if ($bits) {
|
||||
my $mask = (0xff << (8 - $bits)) & 0xff;
|
||||
return 0 if (($addr[$bytes] & $mask) != ($network[$bytes] & $mask));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub get_somaxconn
|
||||
{
|
||||
return defined(&SOMAXCONN) ? SOMAXCONN : 128;
|
||||
|
||||
45
t/miniserv.t
45
t/miniserv.t
@@ -321,6 +321,13 @@ subtest 'canonicalize_ip6' => sub {
|
||||
is($c2, '2001:0db8:0000:0000:0000:0000:0000:0001',
|
||||
'2001:DB8::1 lower-cased and zero-expanded');
|
||||
|
||||
is(miniserv::canonicalize_ip6('::'),
|
||||
'0000:0000:0000:0000:0000:0000:0000:0000',
|
||||
':: expands to 8 zero-padded groups');
|
||||
is(miniserv::canonicalize_ip6('2001:db8::'),
|
||||
'2001:0db8:0000:0000:0000:0000:0000:0000',
|
||||
'trailing :: expands to 8 zero-padded groups');
|
||||
|
||||
# Idempotency: running canonicalize on canonical input returns the same.
|
||||
is(miniserv::canonicalize_ip6($c), $c, 'canonical form is idempotent');
|
||||
};
|
||||
@@ -333,6 +340,44 @@ subtest 'expand_ipv6_bytes' => sub {
|
||||
is($bytes[0], 0, 'high byte is 0 for ::1');
|
||||
};
|
||||
|
||||
subtest 'ip6_prefix_match' => sub {
|
||||
ok( miniserv::ip6_prefix_match('2001:db8::1', '::', 0),
|
||||
'/0 matches any IPv6 address');
|
||||
ok( miniserv::ip6_prefix_match('2001:dbf::1', '2001:db8::', 29),
|
||||
'/29 accepts address inside non-byte-aligned prefix');
|
||||
ok(!miniserv::ip6_prefix_match('2001:dc0::1', '2001:db8::', 29),
|
||||
'/29 rejects address outside non-byte-aligned prefix');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8:ffff::1', '2001:db8::', 32),
|
||||
'/32 matches across the third word');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8:0:1::1', '2001:db8:0:1::', 63),
|
||||
'/63 accepts address inside partial fourth-word prefix');
|
||||
ok(!miniserv::ip6_prefix_match('2001:db8:0:3::1', '2001:db8:0:1::', 63),
|
||||
'/63 rejects address outside partial fourth-word prefix');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8:0:1::1', '2001:db8:0:1::', 64),
|
||||
'/64 matches exact first four words');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8::2', '2001:db8::2', 127),
|
||||
'/127 accepts low address in two-address subnet');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8::3', '2001:db8::2', 127),
|
||||
'/127 accepts high address in two-address subnet');
|
||||
ok(!miniserv::ip6_prefix_match('2001:db8::4', '2001:db8::2', 127),
|
||||
'/127 rejects next subnet');
|
||||
ok( miniserv::ip6_prefix_match('2001:db8::1', '2001:db8::1', 128),
|
||||
'/128 matches identical host address');
|
||||
ok(!miniserv::ip6_prefix_match('2001:db8::2', '2001:db8::1', 128),
|
||||
'/128 rejects neighboring host address');
|
||||
};
|
||||
|
||||
subtest 'ip_match IPv6 CIDR prefixes' => sub {
|
||||
ok( miniserv::ip_match('2001:dbf::1', '2001:db8::1', '2001:db8::/29'),
|
||||
'ip_match accepts non-byte-aligned IPv6 CIDR match');
|
||||
ok(!miniserv::ip_match('2001:dc0::1', '2001:db8::1', '2001:db8::/29'),
|
||||
'ip_match rejects neighboring non-byte-aligned IPv6 CIDR prefix');
|
||||
ok( miniserv::ip_match('2001:db8:0:1::1', '2001:db8::1', '2001:db8:0:1::/63'),
|
||||
'ip_match accepts partial-byte IPv6 prefix');
|
||||
ok(!miniserv::ip_match('2001:db8:0:3::1', '2001:db8::1', '2001:db8:0:1::/63'),
|
||||
'ip_match rejects address outside partial-byte IPv6 prefix');
|
||||
};
|
||||
|
||||
# is_bad_header — ShellShock guard
|
||||
subtest 'is_bad_header' => sub {
|
||||
ok( miniserv::is_bad_header('() { :; }; echo pwned'), 'ShellShock-shaped value flagged');
|
||||
|
||||
@@ -1699,14 +1699,7 @@ for(my $i=1; $i<@_; $i++) {
|
||||
# Compare with an IPv6 network
|
||||
my $v6size = $2;
|
||||
my $v6addr = &canonicalize_ip6($1);
|
||||
my $bytes = $v6size / 8;
|
||||
my @mo = &expand_ipv6_bytes($v6addr);
|
||||
my @io = &expand_ipv6_bytes(&canonicalize_ip6($_[0]));
|
||||
for(my $j=0; $j<$bytes; $j++) {
|
||||
if ($mo[$j] ne $io[$j]) {
|
||||
$mismatch = 1;
|
||||
}
|
||||
}
|
||||
$mismatch = 1 if (!&ip6_prefix_match($_[0], $v6addr, $v6size));
|
||||
}
|
||||
elsif ($_[$i] !~ /^[0-9\.]+$/) {
|
||||
# Compare with hostname
|
||||
@@ -1741,6 +1734,29 @@ foreach my $w (split(/:/, $addr)) {
|
||||
return @rv;
|
||||
}
|
||||
|
||||
=head2 ip6_prefix_match(address, network, prefix)
|
||||
|
||||
Returns 1 if an IPv6 address is in a network prefix.
|
||||
|
||||
=cut
|
||||
sub ip6_prefix_match
|
||||
{
|
||||
my ($addr, $network, $prefix) = @_;
|
||||
my @addr = &expand_ipv6_bytes(&canonicalize_ip6($addr));
|
||||
my @network = &expand_ipv6_bytes(&canonicalize_ip6($network));
|
||||
return 0 if (@addr != 16 || @network != 16 || $prefix < 0 || $prefix > 128);
|
||||
my $bytes = int($prefix / 8);
|
||||
my $bits = $prefix % 8;
|
||||
for(my $i=0; $i<$bytes; $i++) {
|
||||
return 0 if ($addr[$i] != $network[$i]);
|
||||
}
|
||||
if ($bits) {
|
||||
my $mask = (0xff << (8 - $bits)) & 0xff;
|
||||
return 0 if (($addr[$bytes] & $mask) != ($network[$bytes] & $mask));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
=head2 prefix_to_mask(prefix)
|
||||
@@ -1797,8 +1813,6 @@ elsif ($h =~ /^([a-f0-9:]+)\/(\d+)$/) {
|
||||
return &text('access_eip6', $1);
|
||||
$2 >= 0 && $2 <= 128 ||
|
||||
return &text('access_ecidr6', "$2");
|
||||
$2 % 8 == 0 ||
|
||||
return &text('access_ecidr8', "$2");
|
||||
}
|
||||
elsif ($h =~ /^[a-f0-9:]+$/) {
|
||||
# IPv6 address
|
||||
@@ -2635,16 +2649,20 @@ sub canonicalize_ip6
|
||||
{
|
||||
my ($addr) = @_;
|
||||
return $addr if (!&check_ip6address($addr));
|
||||
my @w = split(/:/, $addr);
|
||||
my @w = split(/:/, $addr, -1);
|
||||
my $idx = &indexof("", @w);
|
||||
if ($idx >= 0) {
|
||||
# Expand ::
|
||||
my $mis = 8 - scalar(@w);
|
||||
my @nw = @w[0..$idx];
|
||||
my $endidx = $idx;
|
||||
while($endidx+1 < @w && $w[$endidx+1] eq "") {
|
||||
$endidx++;
|
||||
}
|
||||
my $mis = 8 - (scalar(@w) - ($endidx - $idx + 1));
|
||||
my @nw = @w[0..$idx-1];
|
||||
for(my $i=0; $i<$mis; $i++) {
|
||||
push(@nw, 0);
|
||||
}
|
||||
push(@nw, @w[$idx+1 .. $#w]);
|
||||
push(@nw, @w[$endidx+1 .. $#w]);
|
||||
@w = @nw;
|
||||
}
|
||||
foreach my $w (@w) {
|
||||
|
||||
Reference in New Issue
Block a user