Commit 2f385c10 authored by Frédéric Buclin's avatar Frédéric Buclin

Bug 713926: (CVE-2014-1517) [SECURITY] Login form lacks CSRF protection

r=dkl r=LpSolit a=justdave
parent 88355207
...@@ -152,7 +152,7 @@ sub _handle_login_result { ...@@ -152,7 +152,7 @@ sub _handle_login_result {
if ($self->{_info_getter}->{successful}->requires_persistence if ($self->{_info_getter}->{successful}->requires_persistence
and !Bugzilla->request_cache->{auth_no_automatic_login}) 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) { elsif ($fail_code == AUTH_ERROR) {
......
...@@ -14,19 +14,52 @@ use Bugzilla::Constants; ...@@ -14,19 +14,52 @@ use Bugzilla::Constants;
use Bugzilla::WebService::Constants; use Bugzilla::WebService::Constants;
use Bugzilla::Util; use Bugzilla::Util;
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::Token;
sub get_login_info { sub get_login_info {
my ($self) = @_; my ($self) = @_;
my $params = Bugzilla->input_params; 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 $valid = 0;
my $password = delete $params->{"Bugzilla_password"}; # 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 { failure => AUTH_NODATA };
} }
return { username => $username, password => $password }; return { username => $login, password => $password };
} }
sub fail_nodata { sub fail_nodata {
......
...@@ -52,6 +52,10 @@ sub persist_login { ...@@ -52,6 +52,10 @@ sub persist_login {
$dbh->bz_commit_transaction(); $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. # Prevent JavaScript from accessing login cookies.
my %cookieargs = ('-httponly' => 1); my %cookieargs = ('-httponly' => 1);
......
...@@ -286,6 +286,7 @@ sub close_standby_message { ...@@ -286,6 +286,7 @@ sub close_standby_message {
# Override header so we can add the cookies in # Override header so we can add the cookies in
sub header { sub header {
my $self = shift; my $self = shift;
my $user = Bugzilla->user;
# If there's only one parameter, then it's a Content-Type. # If there's only one parameter, then it's a Content-Type.
if (scalar(@_) == 1) { if (scalar(@_) == 1) {
...@@ -293,6 +294,18 @@ sub header { ...@@ -293,6 +294,18 @@ sub header {
unshift(@_, '-type' => shift(@_)); 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 # Add the cookies in if we have any
if (scalar(@{$self->{Bugzilla_cookie_list}})) { if (scalar(@{$self->{Bugzilla_cookie_list}})) {
unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list}); unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
......
...@@ -889,6 +889,11 @@ sub create { ...@@ -889,6 +889,11 @@ sub create {
# Allow templates to generate a token themselves. # Allow templates to generate a token themselves.
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token, '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. # A way for all templates to get at Field data, cached.
'bug_fields' => sub { 'bug_fields' => sub {
my $cache = Bugzilla->request_cache; my $cache = Bugzilla->request_cache;
......
...@@ -13,8 +13,8 @@ use base qw(Exporter); ...@@ -13,8 +13,8 @@ use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed
html_quote url_quote xml_quote html_quote url_quote xml_quote
css_class_quote html_light_quote css_class_quote html_light_quote
i_am_cgi correct_urlbase remote_ip validate_ip i_am_cgi i_am_webservice correct_urlbase remote_ip
do_ssl_redirect_if_required use_attachbase validate_ip do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db say diff_arrays on_main_db say
trim wrap_hard wrap_comment find_wrap_point trim wrap_hard wrap_comment find_wrap_point
format_time validate_date validate_time datetime_from format_time validate_date validate_time datetime_from
...@@ -230,6 +230,12 @@ sub i_am_cgi { ...@@ -230,6 +230,12 @@ sub i_am_cgi {
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0; 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 # 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 # because we don't want to create a CGI object during XML-RPC calls
# (doing so can mess up XML-RPC). # (doing so can mess up XML-RPC).
...@@ -862,6 +868,7 @@ Bugzilla::Util - Generic utility functions for bugzilla ...@@ -862,6 +868,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
# Functions that tell you about your environment # Functions that tell you about your environment
my $is_cgi = i_am_cgi(); my $is_cgi = i_am_cgi();
my $is_webservice = i_am_webservice();
my $urlbase = correct_urlbase(); my $urlbase = correct_urlbase();
# Data manipulation # Data manipulation
...@@ -989,6 +996,11 @@ Tells you whether or not you are being run as a CGI script in a web ...@@ -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 server. For example, it would return false if the caller is running
in a command-line script. 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()> =item C<correct_urlbase()>
Returns either the C<sslbase> or C<urlbase> parameter, depending on the Returns either the C<sslbase> or C<urlbase> parameter, depending on the
......
...@@ -128,9 +128,7 @@ There are various ways to log in: ...@@ -128,9 +128,7 @@ There are various ways to log in:
=item C<User.login> =item C<User.login>
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla 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 user. This issues a token that you must then use in future calls.
calls, so your client must be capable of receiving and transmitting
cookies.
=item C<Bugzilla_login> and C<Bugzilla_password> =item C<Bugzilla_login> and C<Bugzilla_password>
...@@ -150,19 +148,21 @@ WebService method to perform a login: ...@@ -150,19 +148,21 @@ WebService method to perform a login:
=item C<Bugzilla_restrictlogin> (boolean) - Optional. If true, =item C<Bugzilla_restrictlogin> (boolean) - Optional. If true,
then your login will only be valid for your IP address. 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 =back
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options The C<Bugzilla_restrictlogin> option is only used when you have also
are only used when you have also specified C<Bugzilla_login> and specified C<Bugzilla_login> and C<Bugzilla_password>.
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 Support for using login cookies for authentication has been dropped
response when you use these arguments (just like the C<User.login> method for security reasons.
above).
=back =back
......
...@@ -52,7 +52,6 @@ use constant MAPPED_RETURNS => { ...@@ -52,7 +52,6 @@ use constant MAPPED_RETURNS => {
sub login { sub login {
my ($self, $params) = @_; my ($self, $params) = @_;
my $remember = $params->{remember};
# Username and password params are required # Username and password params are required
foreach my $param ("login", "password") { foreach my $param ("login", "password") {
...@@ -60,33 +59,18 @@ sub login { ...@@ -60,33 +59,18 @@ sub login {
|| ThrowCodeError('param_required', { param => $param }); || 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. # Make sure the CGI user info class works if necessary.
my $input_params = Bugzilla->input_params; my $input_params = Bugzilla->input_params;
$input_params->{'Bugzilla_login'} = $params->{login}; $input_params->{'Bugzilla_login'} = $params->{login};
$input_params->{'Bugzilla_password'} = $params->{password}; $input_params->{'Bugzilla_password'} = $params->{password};
$input_params->{'Bugzilla_remember'} = $remember; $input_params->{'Bugzilla_restrictlogin'} = $params->{restrict_login};
my $user = Bugzilla->login(); my $user = Bugzilla->login();
my $result = { id => $self->type('int', $user->id) }; my $result = { id => $self->type('int', $user->id) };
# We will use the stored cookie value combined with the user id if ($user->{_login_token}) {
# to create a token that can be used with future requests in the $result->{'token'} = $user->id . "-" . $user->{_login_token};
# 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;
} }
return $result; return $result;
...@@ -440,24 +424,19 @@ etc. This method logs in an user. ...@@ -440,24 +424,19 @@ etc. This method logs in an user.
=item C<password> (string) - The user's password. =item C<password> (string) - The user's password.
=item C<remember> (bool) B<Optional> - if the cookies returned by the =item C<restrict_login> (bool) B<Optional> - If set to a true value,
call to login should expire with the session or not. In order for the token returned by this method will only be valid from the IP address
this option to have effect the Bugzilla server must be configured to which called this method.
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.
=back =back
=item B<Returns> =item B<Returns>
On success, a hash containing two items, C<id>, the numeric id of the 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 user that was logged in, and a C<token> which can be passed in the parameters
the parameters as authentication in other calls. A set of http cookies as authentication in other calls. The token can be sent along with any future
is also sent with the response. These cookies *or* the token can be sent requests to the webservice, for the duration of the session, i.e. till
along with any future requests to the webservice, for the duration of the L<User.logout|/logout> is called.
session.
=item B<Errors> =item B<Errors>
...@@ -483,6 +462,19 @@ A login or password parameter was not provided. ...@@ -483,6 +462,19 @@ A login or password parameter was not provided.
=back =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 =back
=head2 logout =head2 logout
......
...@@ -51,6 +51,19 @@ elsif ($action eq 'prepare-sudo') { ...@@ -51,6 +51,19 @@ elsif ($action eq 'prepare-sudo') {
# Keep a temporary record of the user visiting this page # Keep a temporary record of the user visiting this page
$vars->{'token'} = issue_session_token('sudo_prepared'); $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 # Show the sudo page
$vars->{'target_login_default'} = $cgi->param('target_login'); $vars->{'target_login_default'} = $cgi->param('target_login');
$vars->{'reason_default'} = $cgi->param('reason'); $vars->{'reason_default'} = $cgi->param('reason');
......
...@@ -56,6 +56,8 @@ ...@@ -56,6 +56,8 @@
[%+ "checked" IF Param('rememberlogin') == "defaulton" %]> [%+ "checked" IF Param('rememberlogin') == "defaulton" %]>
<label for="Bugzilla_remember[% qs_suffix %]">Remember</label> <label for="Bugzilla_remember[% qs_suffix %]">Remember</label>
[% END %] [% END %]
<input type="hidden" name="Bugzilla_login_token"
value="[% get_login_request_token() FILTER html %]">
<input type="submit" name="GoAheadAndLogIn" value="Log in" <input type="submit" name="GoAheadAndLogIn" value="Log in"
id="log_in[% qs_suffix %]"> id="log_in[% qs_suffix %]">
<script type="text/javascript"> <script type="text/javascript">
......
...@@ -68,6 +68,8 @@ ...@@ -68,6 +68,8 @@
[% PROCESS "global/hidden-fields.html.tmpl" [% PROCESS "global/hidden-fields.html.tmpl"
exclude="^Bugzilla_(login|password|restrictlogin)$" %] 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"> <input type="submit" name="GoAheadAndLogIn" value="Log in" id="log_in">
<p> <p>
......
...@@ -67,9 +67,10 @@ ...@@ -67,9 +67,10 @@
<p> <p>
Finally, enter <label for="Bugzilla_password">your [% terms.Bugzilla %] Finally, enter <label for="Bugzilla_password">your [% terms.Bugzilla %]
password</label>: password</label>:
<input type="hidden" name="Bugzilla_login" value=" <input type="hidden" name="Bugzilla_login" value="[% user.login FILTER html %]">
[%- user.login FILTER html %]">
<input type="password" id="Bugzilla_password" name="Bugzilla_password" size="20"> <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> <br>
This is done for two reasons. First of all, it is done to reduce 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 the chances of someone doing large amounts of damage using your
......
...@@ -217,6 +217,15 @@ ...@@ -217,6 +217,15 @@
[% Hook.process("auth_failure") %] [% 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" %] [% ELSIF error == "attachment_deletion_disabled" %]
[% title = "Attachment Deletion Disabled" %] [% title = "Attachment Deletion Disabled" %]
Attachment deletion is disabled on this installation. Attachment deletion is disabled on this installation.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment