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 {
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) {
......
......@@ -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 {
......
......@@ -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);
......
......@@ -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});
......
......@@ -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;
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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');
......
......@@ -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 = {
......
......@@ -68,8 +68,10 @@
[% 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.)
......
......@@ -67,9 +67,10 @@
<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
......
......@@ -217,6 +217,15 @@
[% 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.
......
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