Commit dc51769c authored by lpsolit%gmail.com's avatar lpsolit%gmail.com

Bug 26257: [SECURITY] Bugzilla should prevent malicious webpages from making…

Bug 26257: [SECURITY] Bugzilla should prevent malicious webpages from making bugzilla users submit changes to bugs - Patch by Fré©ric Buclin <LpSolit@gmail.com> r=mkanat a=LpSolit
parent 8d70890d
...@@ -32,6 +32,7 @@ use strict; ...@@ -32,6 +32,7 @@ use strict;
use Bugzilla::Constants; use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc); use Bugzilla::Install::Util qw(bin_loc);
use Bugzilla::Util qw(generate_random_password);
use Data::Dumper; use Data::Dumper;
use File::Basename qw(dirname); use File::Basename qw(dirname);
...@@ -185,6 +186,18 @@ EOT ...@@ -185,6 +186,18 @@ EOT
# Please specify the directory name only; do not use trailing slash. # Please specify the directory name only; do not use trailing slash.
EOT EOT
}, },
{
name => 'site_wide_secret',
default => generate_random_password(256),
desc => <<EOT
# This secret key is used by your installation for the creation and
# validation of encrypted tokens to prevent unsolicited changes,
# such as bug changes. A random string is generated by default.
# It's very important that this key is kept secret. It also must be
# very long.
EOT
},
); );
sub read_localconfig { sub read_localconfig {
......
...@@ -41,6 +41,7 @@ use Bugzilla::Util; ...@@ -41,6 +41,7 @@ use Bugzilla::Util;
use Bugzilla::User; use Bugzilla::User;
use Bugzilla::Error; use Bugzilla::Error;
use Bugzilla::Status; use Bugzilla::Status;
use Bugzilla::Token;
use Bugzilla::Template::Parser; use Bugzilla::Template::Parser;
use Cwd qw(abs_path); use Cwd qw(abs_path);
...@@ -765,6 +766,9 @@ sub create { ...@@ -765,6 +766,9 @@ sub create {
return $docs_urlbase; return $docs_urlbase;
}, },
# Allow templates to generate a token themselves.
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
# These don't work as normal constants. # These don't work as normal constants.
DB_MODULE => \&Bugzilla::Constants::DB_MODULE, DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
REQUIRED_MODULES => REQUIRED_MODULES =>
......
...@@ -39,10 +39,12 @@ use Bugzilla::User; ...@@ -39,10 +39,12 @@ use Bugzilla::User;
use Date::Format; use Date::Format;
use Date::Parse; use Date::Parse;
use File::Basename; use File::Basename;
use Digest::MD5 qw(md5_hex);
use base qw(Exporter); use base qw(Exporter);
@Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token); @Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token
issue_hash_token check_hash_token);
################################################################################ ################################################################################
# Public Functions # Public Functions
...@@ -170,6 +172,53 @@ sub issue_session_token { ...@@ -170,6 +172,53 @@ sub issue_session_token {
return _create_token(Bugzilla->user->id, 'session', $data); return _create_token(Bugzilla->user->id, 'session', $data);
} }
sub issue_hash_token {
my ($data, $time) = @_;
$data ||= [];
$time ||= time();
# The concatenated string is of the form
# token creation time + site-wide secret + user ID + data
my @args = ($time, Bugzilla->localconfig->{'site_wide_secret'}, Bugzilla->user->id, @$data);
my $token = md5_hex(join('*', @args));
# Prepend the token creation time, unencrypted, so that the token
# lifetime can be validated.
return $time . '-' . $token;
}
sub check_hash_token {
my ($token, $data) = @_;
$data ||= [];
my ($time, $expected_token);
if ($token) {
($time, undef) = split(/-/, $token);
# Regenerate the token based on the information we have.
$expected_token = issue_hash_token($data, $time);
}
if (!$token
|| $expected_token ne $token
|| time() - $time > MAX_TOKEN_AGE * 86400)
{
my $template = Bugzilla->template;
my $vars = {};
$vars->{'script_name'} = basename($0);
$vars->{'token'} = issue_hash_token($data);
$vars->{'reason'} = (!$token) ? 'missing_token' :
($expected_token ne $token) ? 'invalid_token' :
'expired_token';
print Bugzilla->cgi->header();
$template->process('global/confirm-action.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
exit;
}
# If we come here, then the token is valid and not too old.
return 1;
}
sub CleanTokenTable { sub CleanTokenTable {
my $dbh = Bugzilla->dbh; my $dbh = Bugzilla->dbh;
$dbh->do('DELETE FROM tokens $dbh->do('DELETE FROM tokens
...@@ -310,7 +359,7 @@ sub delete_token { ...@@ -310,7 +359,7 @@ sub delete_token {
# Note: this routine must not be called while tables are locked as it will try # Note: this routine must not be called while tables are locked as it will try
# to lock some tables itself, see CleanTokenTable(). # to lock some tables itself, see CleanTokenTable().
sub check_token_data { sub check_token_data {
my ($token, $expected_action) = @_; my ($token, $expected_action, $alternate_script) = @_;
my $user = Bugzilla->user; my $user = Bugzilla->user;
my $template = Bugzilla->template; my $template = Bugzilla->template;
my $cgi = Bugzilla->cgi; my $cgi = Bugzilla->cgi;
...@@ -330,6 +379,7 @@ sub check_token_data { ...@@ -330,6 +379,7 @@ sub check_token_data {
$vars->{'token_action'} = $token_action; $vars->{'token_action'} = $token_action;
$vars->{'expected_action'} = $expected_action; $vars->{'expected_action'} = $expected_action;
$vars->{'script_name'} = basename($0); $vars->{'script_name'} = basename($0);
$vars->{'alternate_script'} = $alternate_script || basename($0);
# Now is a good time to remove old tokens from the DB. # Now is a good time to remove old tokens from the DB.
CleanTokenTable(); CleanTokenTable();
......
...@@ -47,6 +47,7 @@ use Bugzilla::Product; ...@@ -47,6 +47,7 @@ use Bugzilla::Product;
use Bugzilla::Keyword; use Bugzilla::Keyword;
use Bugzilla::Field; use Bugzilla::Field;
use Bugzilla::Status; use Bugzilla::Status;
use Bugzilla::Token;
use Date::Parse; use Date::Parse;
...@@ -1241,6 +1242,7 @@ if ($dotweak && scalar @bugs) { ...@@ -1241,6 +1242,7 @@ if ($dotweak && scalar @bugs) {
} }
$vars->{'dotweak'} = 1; $vars->{'dotweak'} = 1;
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count(); $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
$vars->{'token'} = issue_session_token('buglist_mass_change');
$vars->{'products'} = Bugzilla->user->get_enterable_products; $vars->{'products'} = Bugzilla->user->get_enterable_products;
$vars->{'platforms'} = get_legal_field_values('rep_platform'); $vars->{'platforms'} = get_legal_field_values('rep_platform');
......
...@@ -47,6 +47,7 @@ use Bugzilla::Error; ...@@ -47,6 +47,7 @@ use Bugzilla::Error;
use Bugzilla::Mailer; use Bugzilla::Mailer;
use Bugzilla::User; use Bugzilla::User;
use Bugzilla::Util; use Bugzilla::Util;
use Bugzilla::Token;
############# #############
# Constants # # Constants #
...@@ -201,6 +202,7 @@ sub process_bug { ...@@ -201,6 +202,7 @@ sub process_bug {
$cgi->param(-name => $field, -value => $fields{$field}); $cgi->param(-name => $field, -value => $fields{$field});
} }
$cgi->param('longdesclength', scalar $bug->longdescs); $cgi->param('longdesclength', scalar $bug->longdescs);
$cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts]));
require 'process_bug.cgi'; require 'process_bug.cgi';
} }
......
...@@ -59,6 +59,7 @@ use Bugzilla::Component; ...@@ -59,6 +59,7 @@ use Bugzilla::Component;
use Bugzilla::Keyword; use Bugzilla::Keyword;
use Bugzilla::Flag; use Bugzilla::Flag;
use Bugzilla::Status; use Bugzilla::Status;
use Bugzilla::Token;
use Storable qw(dclone); use Storable qw(dclone);
...@@ -158,10 +159,6 @@ if (defined $cgi->param('dontchange')) { ...@@ -158,10 +159,6 @@ if (defined $cgi->param('dontchange')) {
# reference to flags if $cgi->param('id') is undefined. # reference to flags if $cgi->param('id') is undefined.
Bugzilla::Flag::validate($cgi->param('id')); Bugzilla::Flag::validate($cgi->param('id'));
######################################################################
# End Data/Security Validation
######################################################################
print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL; print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL;
# Check for a mid-air collision. Currently this only works when updating # Check for a mid-air collision. Currently this only works when updating
...@@ -184,6 +181,8 @@ if (defined $cgi->param('delta_ts') ...@@ -184,6 +181,8 @@ if (defined $cgi->param('delta_ts')
$vars->{'comments'} = Bugzilla::Bug::GetComments($first_bug->id, $vars->{'comments'} = Bugzilla::Bug::GetComments($first_bug->id,
"oldest_to_newest"); "oldest_to_newest");
$vars->{'bug'} = $first_bug; $vars->{'bug'} = $first_bug;
# The token contains the old delta_ts. We need a new one.
$cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
# Warn the user about the mid-air collision and ask them what to do. # Warn the user about the mid-air collision and ask them what to do.
$template->process("bug/process/midair.html.tmpl", $vars) $template->process("bug/process/midair.html.tmpl", $vars)
...@@ -191,6 +190,22 @@ if (defined $cgi->param('delta_ts') ...@@ -191,6 +190,22 @@ if (defined $cgi->param('delta_ts')
exit; exit;
} }
# We couldn't do this check earlier as we first had to validate bug IDs
# and display the mid-air collision page if delta_ts changed.
# If we do a mass-change, we use session tokens.
my $token = $cgi->param('token');
if ($cgi->param('id')) {
check_hash_token($token, [$first_bug->id, $first_bug->delta_ts]);
}
else {
check_token_data($token, 'buglist_mass_change', 'query.cgi');
}
######################################################################
# End Data/Security Validation
######################################################################
$vars->{'title_tag'} = "bug_processed"; $vars->{'title_tag'} = "bug_processed";
# Set up the vars for navigational <link> elements # Set up the vars for navigational <link> elements
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
# token_action: the action the token was supposed to serve. # token_action: the action the token was supposed to serve.
# expected_action: the action the user was going to do. # expected_action: the action the user was going to do.
# script_name: the script generating this warning. # script_name: the script generating this warning.
# alternate_script: the suggested script to redirect the user to
# if he declines submission.
#%] #%]
[% PROCESS "global/field-descs.none.tmpl" %] [% PROCESS "global/field-descs.none.tmpl" %]
...@@ -89,8 +91,8 @@ ...@@ -89,8 +91,8 @@
exclude="^(Bugzilla_login|Bugzilla_password)$" %] exclude="^(Bugzilla_login|Bugzilla_password)$" %]
<input type="submit" id="confirm" value="Confirm Changes"> <input type="submit" id="confirm" value="Confirm Changes">
</form> </form>
<p>Or throw away these changes and go back to <a href="[% script_name FILTER html %]"> <p>Or throw away these changes and go back to <a href="[% alternate_script FILTER html %]">
[%- script_name FILTER html %]</a>.</p> [%- alternate_script FILTER html %]</a>.</p>
[% END %] [% END %]
[% PROCESS global/footer.html.tmpl %] [% PROCESS global/footer.html.tmpl %]
...@@ -144,6 +144,7 @@ ...@@ -144,6 +144,7 @@
<input type="hidden" name="delta_ts" value="[% bug.delta_ts %]"> <input type="hidden" name="delta_ts" value="[% bug.delta_ts %]">
<input type="hidden" name="longdesclength" value="[% bug.longdescs.size %]"> <input type="hidden" name="longdesclength" value="[% bug.longdescs.size %]">
<input type="hidden" name="id" value="[% bug.bug_id %]"> <input type="hidden" name="id" value="[% bug.bug_id %]">
<input type="hidden" name="token" value="[% issue_hash_token([bug.id, bug.delta_ts]) FILTER html %]">
[% PROCESS section_title %] [% PROCESS section_title %]
<table> <table>
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
[% dontchange = "--do_not_change--" %] [% dontchange = "--do_not_change--" %]
<input type="hidden" name="dontchange" value="[% dontchange FILTER html %]"> <input type="hidden" name="dontchange" value="[% dontchange FILTER html %]">
<input type="hidden" name="token" value="[% token FILTER html %]">
<script type="text/javascript"> <script type="text/javascript">
var numelements = document.forms.changeform.elements.length; var numelements = document.forms.changeform.elements.length;
......
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