summaryrefslogtreecommitdiffstats
path: root/Bugzilla/Util.pm
diff options
context:
space:
mode:
authorFrédéric Buclin <LpSolit@gmail.com>2012-04-18 18:44:32 +0200
committerFrédéric Buclin <LpSolit@gmail.com>2012-04-18 18:44:32 +0200
commit8dd0e8193d51f243b547cc0f4f21f5b3a1375ff2 (patch)
treee8dc402cbcbb88315400aab3d5bc1287b111539e /Bugzilla/Util.pm
parentb2d4b6c8ca356ae75f8e2e69dcb8f7c6e713b94b (diff)
downloadbugzilla-8dd0e8193d51f243b547cc0f4f21f5b3a1375ff2.tar.gz
bugzilla-8dd0e8193d51f243b547cc0f4f21f5b3a1375ff2.tar.xz
Bug 728639: (CVE-2012-0465) [SECURITY] User lockout policy can be bypassed by altering the X-FORWARDED-FOR header
r=glob a=LpSolit
Diffstat (limited to 'Bugzilla/Util.pm')
-rw-r--r--Bugzilla/Util.pm108
1 files changed, 105 insertions, 3 deletions
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index 91d40a4f2..097fc49fe 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,7 +13,7 @@ use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote
- i_am_cgi correct_urlbase remote_ip
+ i_am_cgi correct_urlbase remote_ip validate_ip
do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db say
trim wrap_hard wrap_comment find_wrap_point
@@ -262,12 +262,103 @@ sub correct_urlbase {
sub remote_ip {
my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'});
- if (first { $_ eq $ip } @proxies) {
- $ip = $ENV{'HTTP_X_FORWARDED_FOR'} if $ENV{'HTTP_X_FORWARDED_FOR'};
+
+ # If the IP address is one of our trusted proxies, then we look at
+ # the X-Forwarded-For header to determine the real remote IP address.
+ if ($ENV{'HTTP_X_FORWARDED_FOR'} && first { $_ eq $ip } @proxies) {
+ my @ips = split(/[\s,]+/, $ENV{'HTTP_X_FORWARDED_FOR'});
+ # This header can contain several IP addresses. We want the
+ # IP address of the machine which connected to our proxies as
+ # all other IP addresses may be fake or internal ones.
+ # Note that this may block a whole external proxy, but we have
+ # no way to determine if this proxy is malicious or trustable.
+ foreach my $remote_ip (reverse @ips) {
+ if (!first { $_ eq $remote_ip } @proxies) {
+ # Keep the original IP address if the remote IP is invalid.
+ $ip = validate_ip($remote_ip) || $ip;
+ last;
+ }
+ }
}
return $ip;
}
+sub validate_ip {
+ my $ip = shift;
+ return is_ipv4($ip) || is_ipv6($ip);
+}
+
+# Copied from Data::Validate::IP::is_ipv4().
+sub is_ipv4 {
+ my $ip = shift;
+ return unless defined $ip;
+
+ my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+ return unless scalar(@octets) == 4;
+
+ foreach my $octet (@octets) {
+ return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
+ }
+
+ # The IP address is valid and can now be detainted.
+ return join('.', @octets);
+}
+
+# Copied from Data::Validate::IP::is_ipv6().
+sub is_ipv6 {
+ my $ip = shift;
+ return unless defined $ip;
+
+ # If there is a :: then there must be only one :: and the length
+ # can be variable. Without it, the length must be 8 groups.
+ my @chunks = split(':', $ip);
+
+ # Need to check if the last chunk is an IPv4 address, if it is we
+ # pop it off and exempt it from the normal IPv6 checking and stick
+ # it back on at the end. If there is only one chunk and it's an IPv4
+ # address, then it isn't an IPv6 address.
+ my $ipv4;
+ my $expected_chunks = 8;
+ if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
+ $ipv4 = pop(@chunks);
+ $expected_chunks--;
+ }
+
+ my $empty = 0;
+ # Workaround to handle trailing :: being valid.
+ if ($ip =~ /[0-9a-f]{1,4}::$/) {
+ $empty++;
+ # Single trailing ':' is invalid.
+ } elsif ($ip =~ /:$/) {
+ return;
+ }
+
+ foreach my $chunk (@chunks) {
+ return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
+ $empty++ if $chunk eq '';
+ }
+ # More than one :: block is bad, but if it starts with :: it will
+ # look like two, so we need an exception.
+ if ($empty == 2 && $ip =~ /^::/) {
+ # This is ok
+ } elsif ($empty > 1) {
+ return;
+ }
+
+ push(@chunks, $ipv4) if $ipv4;
+ # Need 8 chunks, or we need an empty section that could be filled
+ # to represent the missing '0' sections.
+ return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
+
+ my $ipv6 = join(':', @chunks);
+ # The IP address is valid and can now be detainted.
+ trick_taint($ipv6);
+
+ # Need to handle the exception of trailing :: being valid.
+ return "${ipv6}::" if $ip =~ /::$/;
+ return $ipv6;
+}
+
sub use_attachbase {
my $attachbase = Bugzilla->params->{'attachment_base'};
return ($attachbase ne ''
@@ -880,6 +971,17 @@ in a command-line script.
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
current setting for the C<ssl_redirect> parameter.
+=item C<remote_ip()>
+
+Returns the IP address of the remote client. If Bugzilla is behind
+a trusted proxy, it will get the remote IP address by looking at the
+X-Forwarded-For header.
+
+=item C<validate_ip($ip)>
+
+Returns the sanitized IP address if it is a valid IPv4 or IPv6 address,
+else returns undef.
+
=item C<use_attachbase()>
Returns true if an alternate host is used to display attachments; false