Permalink
Browse files

Add IPv6 support to TrustProxy and RobotIP; normalize remote_addr

In order to handle the masking of IPv6 addresses properly, we must first normalize the address to a
full, expanded form.  This ends up changing (potentially and probably) how the internal
representation of $CGI::remote_addr is handled, due to expanding the address to its maximal
representation, with all leading 0's included and no :: truncation present.

This may have side effects to users who are relying on the logging of the IP address in various
tables, as [data session ohost] will return a string which is 39 characters which may exceed some
previous limitations in default character size.  (In the Strap catalog, this includes the
pay_certs_lock table and there are likely many other custom logging solutions for IP which might
need to be expanded.)

Additionally, because this normalization is done prior to session initialization, it is likely that
any existing IPv6 sessions could drop due to a different internal MV_SESSION_ID representation.
Rollout of this should be coordinated to be at a convenient time, or one could write a script to
process/link all session files which could be affected to include the IPv6 as a hard link to the new
session file.

The CIDR representation for IPv6 does not need to be in the normalized form, as it will expand
automatically.  You will also be able to intermix both IPv4 and IPv6 entries for each directive type.
  • Loading branch information...
machack666 committed Dec 5, 2018
1 parent c23beac commit a9d89ee75d397905d306b9818126245aca1cdde8
Showing with 145 additions and 7 deletions.
  1. +86 −1 lib/Vend/CIDR.pm
  2. +2 −2 lib/Vend/Config.pm
  3. +6 −1 lib/Vend/Server.pm
  4. +51 −3 t/cidr.t
@@ -5,11 +5,20 @@ use warnings;
use base qw/Exporter/;
our @EXPORT_OK = qw/cidr2regex/;
our @EXPORT_OK = qw/cidr2regex normalize_ip resembles_ip resembles_cidr/;
sub cidr2regex {
local $_ = shift;
/^(.+)\/\d+/;
return unless resembles_ip($1);
return cidr2regex_ip4($_) if /\./;
return cidr2regex_ip6($_);
}
sub cidr2regex_ip4 {
local $_ = shift;
return unless /([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\/([0-9]{1,2})/;
my $map = {
@@ -319,4 +328,80 @@ sub cidr2regex {
return $act->{$5}->();
}
sub cidr2regex_ip6 {
local $_ = shift;
return unless /^([:a-f0-9]+)\/(\d+)$/i;
my $masklen = $2;
return unless $masklen <= 128;
return unless my $norm = normalize_ip6($1);
return $norm if $masklen == 128; # fixed IP
return '.*' if $masklen == 0; # open mask
# nybble count
my $nyblen = $masklen >> 2;
# remainder to determine regex
my $nybmod = $masklen & 0x3;
my $ncolons = $nyblen >> 2;
my $pat_lkup = [
[""],
["[0-7]","[89a-f]"],
["[0-3]","[4-7]","[89ab]","[c-f]"],
["[01]","[23]","[45]","[67]","[89]","[ab]","[cd]","[ef]"],
];
my $chr = hex(substr($norm, $ncolons + $nyblen, 1));
my $pat = $pat_lkup->[$nybmod]->[$chr >> (4-$nybmod)];
return sprintf '%s%s.*', substr($norm, 0, $ncolons + $nyblen), $pat;
}
sub normalize_ip {
local $_ = shift;
return unless resembles_ip($_);
return $_ if /^\d+\.\d+\.\d+\.\d+$/;
return normalize_ip6($_);
}
sub normalize_ip6 {
my ($src_addr) = @_;
if ($src_addr =~ /^[:a-f0-9]+$/i) {
my $has_dbl_colon = scalar (@{[$src_addr =~ /::/g]});
return if $src_addr =~ /:::/ or $has_dbl_colon > 1;
my @parts = split '::' => $src_addr;
my @norm;
if (@parts <= 2) {
my @p1 = split ':' => $parts[0] if defined $parts[0];
my @p2 = split ':' => $parts[1] if defined $parts[1];
my $tot = @p1 + @p2;
if ((@p1 == 8 || $has_dbl_colon) && $tot <= 8) {
my $zeros = 8 - $tot;
for my $part (@p1, ('0000') x $zeros, @p2) {
return if $part !~ /^[a-f0-9]{1,4}$/i;
push @norm, sprintf '%04s', lc($part); # normalize component
}
return join ':' => @norm;
}
}
}
return;
}
sub resembles_ip {
return $_[0] =~ /^(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[:a-f0-9]+)$/i;
}
sub resembles_cidr {
return $_[0] =~ /^([a-f0-9.:]+)\/\d+$/i && resembles_ip($1);
}
1;
@@ -53,7 +53,7 @@ use Vend::File;
use Vend::Data;
use Vend::Cron;
use Vend::CharSet ();
use Vend::CIDR qw(cidr2regex);
use Vend::CIDR qw(cidr2regex resembles_cidr);
$VERSION = '2.251';
@@ -3872,7 +3872,7 @@ sub parse_list_wildcard_cidr {
my (@components, @other);
for (split /\s*,\s*/ => $value) {
if (/^\d+\.\d+\.\d+\.\d+\/\d+$/) {
if (resembles_cidr($_)) {
push @components, cidr2regex($_);
}
else {
@@ -30,6 +30,7 @@ use Cwd;
use POSIX qw(setsid strftime);
use Vend::Util;
use Vend::CharSet qw/ to_internal decode_urlencode default_charset /;
use Vend::CIDR qw( normalize_ip resembles_ip );
use Fcntl;
use Errno qw/:POSIX/;
use Config;
@@ -141,6 +142,8 @@ sub populate {
#::logDebug("CGI::$field=" . ${"CGI::$field"});
}
$CGI::remote_addr = normalize_ip($CGI::remote_addr);
# try to get originating host's IP address if request was
# forwarded through a trusted proxy
if (
@@ -155,7 +158,7 @@ sub populate {
# in a comma-separated list
for my $ip (reverse grep /\S/, split /\s*,\s*/, $forwarded_for) {
# do we have a valid-looking IP address?
if ($ip !~ /^\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?$/) {
if (!resembles_ip($ip)) {
# if not, log error and ignore X-Forwarded-For header
::logGlobal(
{ level => 'info' },
@@ -166,6 +169,8 @@ sub populate {
last;
}
$ip = normalize_ip($ip);
# skip any other upstream trusted proxies
next if $ip =~ $Global::TrustProxy;
@@ -1,9 +1,36 @@
# -*- cperl -*-
use strict;
use warnings;
use lib 'lib';
use Test::More;
use Vend::CIDR qw(cidr2regex);
use Vend::CIDR qw(cidr2regex normalize_ip resembles_ip);
my @invalid_ips = (
':::',
'::1::',
'123:::123',
'1:2:3:4:5:6:7:8:9',
'1.2.3',
'1234.1234.123.1234',
'123.234.123.234.12',
);
my %ip_norm_tests = (
'::' => '0000:0000:0000:0000:0000:0000:0000:0000',
'1::' => '0001:0000:0000:0000:0000:0000:0000:0000',
'::1' => '0000:0000:0000:0000:0000:0000:0000:0001',
'abcd::1234' => 'abcd:0000:0000:0000:0000:0000:0000:1234',
'1:2:3:4:5:6:7:8' => '0001:0002:0003:0004:0005:0006:0007:0008',
'2600:42::' => '2600:0042:0000:0000:0000:0000:0000:0000',
'ABCD:C0FF:EFFE::1' => 'abcd:c0ff:effe:0000:0000:0000:0000:0001',
);
#is(resembles_ip($_), 0, "recognized invalid ip $_") for @invalid_ips;
is(normalize_ip($_), undef, "returns nothing for invalid ip $_") for @invalid_ips;
is(normalize_ip($_), $ip_norm_tests{$_}, "normalize IP $_") for sort keys %ip_norm_tests;
# Test IPv4
test_cidr(
['10.0.0.0/24'],
[qw(10.0.0.0 10.0.0.10 10.0.0.255)],
@@ -40,6 +67,27 @@ test_cidr(
[qw(1.1.0.0 2.2.1.1 255.255.0.0)],
);
# Test IPv6
test_cidr(
['1::/16'],
[qw(1:0::)],
[qw(2:1:: ::1)],
);
test_cidr(
['2400:cb00::/32','2405:b500::/32','2606:4700::/32','2803:f800::/32','2c0f:f248::/32','2a06:98c0::/29'],
[qw(2400:cb00::1 2803:f800::32:12 2a06:98c6::56 2a06:98c2::56)],
[qw(1:1:: 2400:cb01:: 2a06:98d0::56)],
);
# Mixed
test_cidr(
['2400:cb00::/32','10.0.0.1/8'],
[qw(2400:cb00::1 2400:cb00:ff::32:12 10.2.23.1)],
[qw(1:1:: 2400:cb01::2a06:98d0::56 12.124.53.39)],
);
done_testing();
sub test_cidr {
@@ -48,6 +96,6 @@ sub test_cidr {
my $pat = join '|' => map { cidr2regex($_) } @$pats;
my $cidr = qr(^($pat)$);
like ($_, $cidr, "$_ matches cidr") for @$include;
unlike($_, $cidr, "$_ doesn't match cidr") for @$exclude;
like (normalize_ip($_), $cidr, "$_ matches cidr") for @$include;
unlike(normalize_ip($_), $cidr, "$_ doesn't match cidr") for @$exclude;
}

0 comments on commit a9d89ee

Please sign in to comment.