]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 713926: (CVE-2014-1517) [SECURITY] Login form lacks CSRF protection
authorFrédéric Buclin <LpSolit@gmail.com>
Thu, 17 Apr 2014 16:18:12 +0000 (18:18 +0200)
committerFrédéric Buclin <LpSolit@gmail.com>
Thu, 17 Apr 2014 16:18:12 +0000 (18:18 +0200)
r=dkl r=LpSolit a=justdave

13 files changed:
Bugzilla/Auth.pm
Bugzilla/Auth/Login/CGI.pm
Bugzilla/Auth/Persist/Cookie.pm
Bugzilla/CGI.pm
Bugzilla/Template.pm
Bugzilla/Util.pm
Bugzilla/WebService.pm
Bugzilla/WebService/User.pm
relogin.cgi
template/en/default/account/auth/login-small.html.tmpl
template/en/default/account/auth/login.html.tmpl
template/en/default/admin/sudo.html.tmpl
template/en/default/global/user-error.html.tmpl

index 05238e0b3e9df58a8e373733b3777c8e452b4575..09a2c1da4821acdfb696dc80a8fd93a16a0a65f8 100644 (file)
@@ -152,7 +152,7 @@ sub _handle_login_result {
         if ($self->{_info_getter}->{successful}->requires_persistence
             and !Bugzilla->request_cache->{auth_no_automatic_login}) 
         {
-            $self->{_persister}->persist_login($user);
+            $user->{_login_token} = $self->{_persister}->persist_login($user);
         }
     }
     elsif ($fail_code == AUTH_ERROR) {
index 47ec556a70a057a978bc2013eecb623ad6515274..090680ebff1dc9a75abbbea9974e84f141991aa9 100644 (file)
@@ -14,19 +14,52 @@ use Bugzilla::Constants;
 use Bugzilla::WebService::Constants;
 use Bugzilla::Util;
 use Bugzilla::Error;
+use Bugzilla::Token;
 
 sub get_login_info {
     my ($self) = @_;
     my $params = Bugzilla->input_params;
+    my $cgi = Bugzilla->cgi;
+
+    my $login = trim(delete $params->{'Bugzilla_login'});
+    my $password = delete $params->{'Bugzilla_password'};
+    # The token must match the cookie to authenticate the request.
+    my $login_token = delete $params->{'Bugzilla_login_token'};
+    my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie');
 
-    my $username = trim(delete $params->{"Bugzilla_login"});
-    my $password = delete $params->{"Bugzilla_password"};
+    my $valid = 0;
+    # If the web browser accepts cookies, use them.
+    if ($login_token && $login_cookie) {
+        my ($time, undef) = split(/-/, $login_token);
+        # Regenerate the token based on the information we have.
+        my $expected_token = issue_hash_token(['login_request', $login_cookie], $time);
+        $valid = 1 if $expected_token eq $login_token;
+        $cgi->remove_cookie('Bugzilla_login_request_cookie');
+    }
+    # WebServices and other local scripts can bypass this check.
+    # This is safe because we won't store a login cookie in this case.
+    elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+        $valid = 1;
+    }
+    # Else falls back to the Referer header and accept local URLs.
+    # Attachments are served from a separate host (ideally), and so
+    # an evil attachment cannot abuse this check with a redirect.
+    elsif (my $referer = $cgi->referer) {
+        my $urlbase = correct_urlbase();
+        $valid = 1 if $referer =~ /^\Q$urlbase\E/;
+    }
+    # If the web browser doesn't accept cookies and the Referer header
+    # is missing, we have no way to make sure that the authentication
+    # request comes from the user.
+    elsif ($login && $password) {
+        ThrowUserError('auth_untrusted_request', { login => $login });
+    }
 
-    if (!defined $username || !defined $password) {
+    if (!$login || !$password || !$valid) {
         return { failure => AUTH_NODATA };
     }
 
-    return { username => $username, password => $password };
+    return { username => $login, password => $password };
 }
 
 sub fail_nodata {
index fe37551c9ca3baa85c385299103550229e78d2b5..b0aeb4f0fa2afca9db3f7d57bd33a25865852164 100644 (file)
@@ -52,6 +52,10 @@ sub persist_login {
 
     $dbh->bz_commit_transaction();
 
+    # We do not want WebServices to generate login cookies.
+    # All we need is the login token for User.login.
+    return $login_cookie if i_am_webservice();
+
     # Prevent JavaScript from accessing login cookies.
     my %cookieargs = ('-httponly' => 1);
 
index dc298a0532629c3786643250caca9a7ac9971c26..7bb9d96c47687eb1d033be85b4a58c342da1bfcc 100644 (file)
@@ -286,6 +286,7 @@ sub close_standby_message {
 # Override header so we can add the cookies in
 sub header {
     my $self = shift;
+    my $user = Bugzilla->user;
 
     # If there's only one parameter, then it's a Content-Type.
     if (scalar(@_) == 1) {
@@ -293,6 +294,18 @@ sub header {
         unshift(@_, '-type' => shift(@_));
     }
 
+    if (!$user->id && $user->authorizer->can_login
+        && !$self->cookie('Bugzilla_login_request_cookie'))
+    {
+        my %args;
+        $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+
+        $self->send_cookie(-name => 'Bugzilla_login_request_cookie',
+                           -value => generate_random_password(),
+                           -httponly => 1,
+                           %args);
+    }
+
     # Add the cookies in if we have any
     if (scalar(@{$self->{Bugzilla_cookie_list}})) {
         unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
index c1f49a22479bb06b1fcc1a1bc8fa078a3ccbe365..44425edaa6fa7e576012a4c045ee8ce61b25fd53 100644 (file)
@@ -889,6 +889,11 @@ sub create {
             # Allow templates to generate a token themselves.
             'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
 
+            'get_login_request_token' => sub {
+                my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
+                return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
+            },
+
             # A way for all templates to get at Field data, cached.
             'bug_fields' => sub {
                 my $cache = Bugzilla->request_cache;
index 72189741375d9637c27220cad126cf824b74469b..15bd7e2203b3461ac9c45193b5d02eae5a58209f 100644 (file)
@@ -13,8 +13,8 @@ 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 validate_ip
-                             do_ssl_redirect_if_required use_attachbase
+                             i_am_cgi i_am_webservice 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
                              format_time validate_date validate_time datetime_from
@@ -230,6 +230,12 @@ sub i_am_cgi {
     return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
 }
 
+sub i_am_webservice {
+    my $usage_mode = Bugzilla->usage_mode;
+    return $usage_mode == USAGE_MODE_XMLRPC
+           || $usage_mode == USAGE_MODE_JSON;
+}
+
 # This exists as a separate function from Bugzilla::CGI::redirect_to_https
 # because we don't want to create a CGI object during XML-RPC calls
 # (doing so can mess up XML-RPC).
@@ -862,6 +868,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
 
   # Functions that tell you about your environment
   my $is_cgi   = i_am_cgi();
+  my $is_webservice = i_am_webservice();
   my $urlbase  = correct_urlbase();
 
   # Data manipulation
@@ -989,6 +996,11 @@ Tells you whether or not you are being run as a CGI script in a web
 server. For example, it would return false if the caller is running
 in a command-line script.
 
+=item C<i_am_webservice()>
+
+Tells you whether or not the current usage mode is WebServices related
+such as JSONRPC or XMLRPC.
+
 =item C<correct_urlbase()>
 
 Returns either the C<sslbase> or C<urlbase> parameter, depending on the
index 4d018772f71ee199927ec86b402903ee28a2de5e..2a0e8890f1208b3e190e3ca362291e45baaff052 100644 (file)
@@ -128,9 +128,7 @@ There are various ways to log in:
 =item C<User.login>
 
 You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla 
-user. This issues standard HTTP cookies that you must then use in future
-calls, so your client must be capable of receiving and transmitting
-cookies.
+user. This issues a token that you must then use in future calls.
 
 =item C<Bugzilla_login> and C<Bugzilla_password>
 
@@ -150,19 +148,21 @@ WebService method to perform a login:
 =item C<Bugzilla_restrictlogin> (boolean) - Optional. If true,
 then your login will only be valid for your IP address.
 
-=item C<Bugzilla_rememberlogin> (boolean) - Optional. If true,
-then the cookie sent back to you with the method response will
-not expire.
-
 =back
 
-The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
-are only used when you have also specified C<Bugzilla_login> and 
-C<Bugzilla_password>.
+The C<Bugzilla_restrictlogin> option is only used when you have also
+specified C<Bugzilla_login> and C<Bugzilla_password>.
+
+=item C<Bugzilla_token>
+
+B<Added in Bugzilla 5.0 and backported to 4.4.3>
+
+You can specify C<Bugzilla_token> as argument to any WebService method,
+and you will be logged in as that user if the token is correct. This is
+the token returned when calling C<User.login> mentioned above.
 
-Note that Bugzilla will return HTTP cookies along with the method
-response when you use these arguments (just like the C<User.login> method
-above).
+Support for using login cookies for authentication has been dropped
+for security reasons.
 
 =back
 
index b01f30338ec925ac3f86e76cec76b824c3d31113..b865e114c384c6d8e64b6d2995da144904710872 100644 (file)
@@ -52,7 +52,6 @@ use constant MAPPED_RETURNS => {
 
 sub login {
     my ($self, $params) = @_;
-    my $remember = $params->{remember};
 
     # Username and password params are required 
     foreach my $param ("login", "password") {
@@ -60,33 +59,18 @@ sub login {
             || ThrowCodeError('param_required', { param => $param });
     }
 
-    # Convert $remember from a boolean 0/1 value to a CGI-compatible one.
-    if (defined($remember)) {
-        $remember = $remember? 'on': '';
-    }
-    else {
-        # Use Bugzilla's default if $remember is not supplied.
-        $remember =
-            Bugzilla->params->{'rememberlogin'} eq 'defaulton'? 'on': '';
-    }
-
     # Make sure the CGI user info class works if necessary.
     my $input_params = Bugzilla->input_params;
     $input_params->{'Bugzilla_login'} =  $params->{login};
     $input_params->{'Bugzilla_password'} = $params->{password};
-    $input_params->{'Bugzilla_remember'} = $remember;
+    $input_params->{'Bugzilla_restrictlogin'} = $params->{restrict_login};
 
     my $user = Bugzilla->login();
 
     my $result = { id => $self->type('int', $user->id) };
 
-    # We will use the stored cookie value combined with the user id
-    # to create a token that can be used with future requests in the
-    # query parameters
-    my $login_cookie = first { $_->name eq 'Bugzilla_logincookie' }
-                              @{ Bugzilla->cgi->{'Bugzilla_cookie_list'} };
-    if ($login_cookie) {
-        $result->{'token'} = $user->id . "-" . $login_cookie->value;
+    if ($user->{_login_token}) {
+        $result->{'token'} = $user->id . "-" . $user->{_login_token};
     }
 
     return $result;
@@ -440,24 +424,19 @@ etc. This method logs in an user.
 
 =item C<password> (string) - The user's password.
 
-=item C<remember> (bool) B<Optional> - if the cookies returned by the
-call to login should expire with the session or not.  In order for
-this option to have effect the Bugzilla server must be configured to
-allow the user to set this option - the Bugzilla parameter
-I<rememberlogin> must be set to "defaulton" or
-"defaultoff". Addionally, the client application must implement
-management of cookies across sessions.
+=item C<restrict_login> (bool) B<Optional> - If set to a true value,
+the token returned by this method will only be valid from the IP address
+which called this method.
 
 =back
 
 =item B<Returns>
 
 On success, a hash containing two items, C<id>, the numeric id of the
-user that was logged in, and a C<token> which can be passed in
-the parameters as authentication in other calls. A set of http cookies
-is also sent with the response. These cookies *or* the token can be sent
-along with any future requests to the webservice, for the duration of the
-session.
+user that was logged in, and a C<token> which can be passed in the parameters
+as authentication in other calls. The token can be sent along with any future
+requests to the webservice, for the duration of the session, i.e. till
+L<User.logout|/logout> is called.
 
 =item B<Errors>
 
@@ -483,6 +462,19 @@ A login or password parameter was not provided.
 
 =back
 
+=item B<History>
+
+=over
+
+=item C<remember> was removed in Bugzilla B<4.4> as this method no longer
+creates a login cookie.
+
+=item C<restrict_login> was added in Bugzilla B<4.4>.
+
+=item C<token> was added in Bugzilla B<4.4>.
+
+=back
+
 =back
 
 =head2 logout
index b3307c9eb5e6b2957634d111660db353dafdf925..4338c8ee041246c5850e927fa18c4a5f632af941 100755 (executable)
@@ -51,6 +51,19 @@ elsif ($action eq 'prepare-sudo') {
     # Keep a temporary record of the user visiting this page
     $vars->{'token'} = issue_session_token('sudo_prepared');
 
+    if ($user->authorizer->can_login) {
+        my $value = generate_random_password();
+        my %args;
+        $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
+
+        $cgi->send_cookie(-name => 'Bugzilla_login_request_cookie',
+                          -value => $value,
+                          -httponly => 1,
+                          %args);
+
+        $vars->{'login_request_token'} = issue_hash_token(['login_request', $value]);
+    }
+
     # Show the sudo page
     $vars->{'target_login_default'} = $cgi->param('target_login');
     $vars->{'reason_default'} = $cgi->param('reason');
index 801fef81ee569b121c9facf9a996b8cb25f37c41..818aa6b720d65c351d8b453a9f6627302562bbbe 100644 (file)
@@ -56,7 +56,9 @@
                  [%+ "checked" IF Param('rememberlogin') == "defaulton" %]>
       <label for="Bugzilla_remember[% qs_suffix %]">Remember</label>
     [% END %]
-    <input type="submit" name="GoAheadAndLogIn" value="Log in" 
+    <input type="hidden" name="Bugzilla_login_token"
+           value="[% get_login_request_token() FILTER html %]">
+    <input type="submit" name="GoAheadAndLogIn" value="Log in"
             id="log_in[% qs_suffix %]">
     <script type="text/javascript">
       mini_login_constants = {
index 0a8a3d3b8fcb1f6825aec6c347a595454f12ca68..77a1ba2d6737417325b0e8331bf4212038038a5e 100644 (file)
   [% PROCESS "global/hidden-fields.html.tmpl"
      exclude="^Bugzilla_(login|password|restrictlogin)$" %]
 
+  <input type="hidden" name="Bugzilla_login_token"
+         value="[% get_login_request_token() FILTER html %]">
   <input type="submit" name="GoAheadAndLogIn" value="Log in" id="log_in">
-  
+
   <p>
     (Note: you should make sure cookies are enabled for this site. 
     Otherwise, you will be required to log in frequently.)
index bacffaeba8d84f71846b127a18c73ee35570e386..4d072f11415e5e6fbc05a29ef46c4ece2e2a5c7b 100644 (file)
     <p>
       Finally, enter <label for="Bugzilla_password">your [% terms.Bugzilla %]
       password</label>:
-      <input type="hidden" name="Bugzilla_login" value="
-      [%- user.login FILTER html %]">
+      <input type="hidden" name="Bugzilla_login" value="[% user.login FILTER html %]">
       <input type="password" id="Bugzilla_password" name="Bugzilla_password" size="20">
+      <input type="hidden" name="Bugzilla_login_token"
+             value="[% login_request_token FILTER html %]">
       <br>
       This is done for two reasons.  First of all, it is done to reduce 
       the chances of someone doing large amounts of damage using your 
index efc3f56cfc771775bb0397870dc4d6bf6d0eccf5..8dbcd45e3915b5e665b43e78ef9ff0f075cc0337 100644 (file)
 
     [% Hook.process("auth_failure") %]
 
+  [% ELSIF error == "auth_untrusted_request" %]
+    [% title = "Untrusted Authentication Request" %]
+    You tried to log in using the <em>[% login FILTER html %]</em> account,
+    but [% terms.Bugzilla %] is unable to trust your request. Make sure
+    your web browser accepts cookies and that you haven't been redirected
+    here from an external web site.
+    <a href="index.cgi?GoAheadAndLogIn=1">Click here</a> if you really want
+    to log in.
+
   [% ELSIF error == "attachment_deletion_disabled" %]
     [% title = "Attachment Deletion Disabled" %]
     Attachment deletion is disabled on this installation.