From: Frédéric Buclin Date: Thu, 17 Apr 2014 16:18:12 +0000 (+0200) Subject: Bug 713926: (CVE-2014-1517) [SECURITY] Login form lacks CSRF protection X-Git-Tag: bugzilla-4.4.3~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2f385c10cd393630e38c48a45ec34f9175e9bf64;p=thirdparty%2Fbugzilla.git Bug 713926: (CVE-2014-1517) [SECURITY] Login form lacks CSRF protection r=dkl r=LpSolit a=justdave --- diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 05238e0b3e..09a2c1da48 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -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) { diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index 47ec556a70..090680ebff 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -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 { diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm index fe37551c9c..b0aeb4f0fa 100644 --- a/Bugzilla/Auth/Persist/Cookie.pm +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -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); diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index dc298a0532..7bb9d96c47 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -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}); diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index c1f49a2247..44425edaa6 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -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; diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm index 7218974137..15bd7e2203 100644 --- a/Bugzilla/Util.pm +++ b/Bugzilla/Util.pm @@ -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 + +Tells you whether or not the current usage mode is WebServices related +such as JSONRPC or XMLRPC. + =item C Returns either the C or C parameter, depending on the diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm index 4d018772f7..2a0e8890f1 100644 --- a/Bugzilla/WebService.pm +++ b/Bugzilla/WebService.pm @@ -128,9 +128,7 @@ There are various ways to log in: =item C You can use L 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 and C @@ -150,19 +148,21 @@ WebService method to perform a login: =item C (boolean) - Optional. If true, then your login will only be valid for your IP address. -=item C (boolean) - Optional. If true, -then the cookie sent back to you with the method response will -not expire. - =back -The C and C options -are only used when you have also specified C and -C. +The C option is only used when you have also +specified C and C. + +=item C + +B + +You can specify C 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 mentioned above. -Note that Bugzilla will return HTTP cookies along with the method -response when you use these arguments (just like the C method -above). +Support for using login cookies for authentication has been dropped +for security reasons. =back diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index b01f30338e..b865e114c3 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -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 (string) - The user's password. -=item C (bool) B - 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 must be set to "defaulton" or -"defaultoff". Addionally, the client application must implement -management of cookies across sessions. +=item C (bool) B - 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 On success, a hash containing two items, C, the numeric id of the -user that was logged in, and a C 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 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 is called. =item B @@ -483,6 +462,19 @@ A login or password parameter was not provided. =back +=item B + +=over + +=item C was removed in Bugzilla B<4.4> as this method no longer +creates a login cookie. + +=item C was added in Bugzilla B<4.4>. + +=item C was added in Bugzilla B<4.4>. + +=back + =back =head2 logout diff --git a/relogin.cgi b/relogin.cgi index b3307c9eb5..4338c8ee04 100755 --- a/relogin.cgi +++ b/relogin.cgi @@ -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'); diff --git a/template/en/default/account/auth/login-small.html.tmpl b/template/en/default/account/auth/login-small.html.tmpl index 801fef81ee..818aa6b720 100644 --- a/template/en/default/account/auth/login-small.html.tmpl +++ b/template/en/default/account/auth/login-small.html.tmpl @@ -56,7 +56,9 @@ [%+ "checked" IF Param('rememberlogin') == "defaulton" %]> [% END %] - +