use strict;
use Bugzilla::Constants;
+use Bugzilla::Util qw(generate_random_password);
use Data::Dumper;
use IO::File;
desc => <<EOT
# The interdiff feature needs diff, so we have to have that path.
# Please specify the directory name only; do not use trailing slash.
+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
},
);
use Bugzilla::Util;
use Bugzilla::User;
use Bugzilla::Error;
-use MIME::Base64;
+use Bugzilla::Token;
use Bugzilla::Bug;
use Cwd qw(abs_path);
# for time2str - replace by TT Date plugin??
use Date::Format ();
+use MIME::Base64;
use File::Basename qw(dirname);
use File::Find;
use File::Path qw(rmtree mkpath);
Bugzilla::BugMail::Send($id, $mailrecipients);
},
+ # Allow templates to generate a token themselves.
+ 'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
+
# These don't work as normal constants.
DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
REQUIRED_MODULES =>
use Date::Format;
use Date::Parse;
use File::Basename;
+use Digest::MD5 qw(md5_hex);
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
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 {
my $dbh = Bugzilla->dbh;
$dbh->bz_lock_tables('tokens WRITE');
use Bugzilla::Product;
use Bugzilla::Keyword;
use Bugzilla::Field;
+use Bugzilla::Token;
use Date::Parse;
$result
|| ThrowUserError("buglist_parameters_required", {'queryname' => $name});
- return $result;
+ return wantarray ? ($result, $id) : $result;
}
# Inserts a Named Query (a "Saved Search") into the database, or
# Take appropriate action based on user's request.
if ($cgi->param('cmdtype') eq "dorem") {
if ($cgi->param('remaction') eq "run") {
- $buffer = LookupNamedQuery(scalar $cgi->param("namedcmd"),
- scalar $cgi->param('sharer_id'));
+ my $query_id;
+ ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"),
+ scalar $cgi->param('sharer_id'));
# If this is the user's own query, remember information about it
# so that it can be modified easily.
$vars->{'searchname'} = $cgi->param('namedcmd');
if (!$cgi->param('sharer_id') ||
$cgi->param('sharer_id') == Bugzilla->user->id) {
$vars->{'searchtype'} = "saved";
+ $vars->{'search_id'} = $query_id;
}
$params = new Bugzilla::CGI($buffer);
$order = $params->param('order') || $order;
# The user has no query of this name. Play along.
}
else {
+ # Make sure the user really wants to delete his saved search.
+ my $token = $cgi->param('token');
+ check_hash_token($token, [$query_id, $qname]);
+
$dbh->do('DELETE FROM namedqueries
WHERE id = ?',
undef, $query_id);
my %bug_ids;
my $is_new_name = 0;
if ($query_name) {
+ my ($query, $query_id) =
+ LookupNamedQuery($query_name, undef, QUERY_LIST, !THROW_ERROR);
# Make sure this name is not already in use by a normal saved search.
- if (LookupNamedQuery($query_name, undef, QUERY_LIST, !THROW_ERROR)) {
- ThrowUserError('query_name_exists', {'name' => $query_name});
+ if ($query) {
+ ThrowUserError('query_name_exists', {name => $query_name,
+ query_id => $query_id});
}
$is_new_name = 1;
}
Remove from <a href="editwhines.cgi">whining</a> first
[% ELSE %]
<a href="buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=
- [% q.name FILTER url_quote %]">Forget</a>
+ [% q.name FILTER url_quote %]&token=
+ [% issue_hash_token([q.id, q.name]) FILTER url_quote %]">Forget</a>
[% END %]
</td>
<td align="center">
--- /dev/null
+[%# 1.0@bugzilla.org %]
+[%# The contents of this file are subject to the Mozilla Public
+ # License Version 1.1 (the "License"); you may not use this file
+ # except in compliance with the License. You may obtain a copy of
+ # the License at http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS
+ # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
+ # implied. See the License for the specific language governing
+ # rights and limitations under the License.
+ #
+ # The Original Code is the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Frédéric Buclin.
+ # Portions created by Frédéric Buclin are Copyright (C) 2008
+ # Frédéric Buclin. All Rights Reserved.
+ #
+ # Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+ #%]
+
+[%# INTERFACE:
+ # script_name: the script generating this warning.
+ # token: a valid token for the current action.
+ # reason: reason of the failure.
+ #%]
+
+[% PROCESS global/header.html.tmpl title = "Suspicious Action"
+ style_urls = ['skins/standard/global.css'] %]
+
+<div class="throw_error">
+ [% IF reason == "expired_token" %]
+ Your changes have been rejected because you exceeded the time limit
+ of [% constants.MAX_TOKEN_AGE FILTER html %] days before submitting your
+ changes to [% script_name FILTER html %]. Your page may have been displayed
+ for too long, or old changes have been resubmitted by accident.
+
+ [% ELSIF reason == "missing_token" %]
+ It looks like you didn't come from the right page.
+ One reason could be that you entered the URL in the address bar of your
+ web browser directly, which should be safe. Another reason could be that
+ you clicked on a URL which redirected you here <b>without your consent</b>.
+
+ [% ELSIF reason == "invalid_token" %]
+ You submitted changes to [% script_name FILTER html %] with an invalid
+ token, which may indicate that someone tried to abuse you, for instance
+ by making you click on a URL which redirected you here <b>without your
+ consent</b>.
+ [% END %]
+ <p>
+ Are you sure you want to commit these changes?
+ </p>
+</div>
+
+<form name="check" id="check" method="post" action="[% script_name FILTER html %]">
+ [% PROCESS "global/hidden-fields.html.tmpl"
+ exclude="^(Bugzilla_login|Bugzilla_password|token)$" %]
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="submit" id="confirm" value="Yes, Confirm Changes">
+</form>
+
+<p><a href="index.cgi">No, throw away these changes</a> (you will be redirected
+to the home page).</p>
+
+[% PROCESS global/footer.html.tmpl %]
The name <em>[% name FILTER html %]</em> is already used by another
saved search. You first have to
<a href="buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=
- [%- name FILTER url_quote %]">delete</a> it if you really want to use
- this name.
+ [%- name FILTER url_quote %]&token=
+ [% issue_hash_token([query_id, name]) FILTER url_quote %]">delete</a>
+ it if you really want to use this name.
[% ELSIF error == "query_name_missing" %]
[% title = "No Search Name Specified" %]
<td valign="middle" nowrap="nowrap">
|
<a href="buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=
- [% searchname FILTER url_quote %]">Forget Search '
- [% searchname FILTER html %]'</a>
+ [% searchname FILTER url_quote %]&token=
+ [% issue_hash_token([search_id, searchname]) FILTER url_quote %]">
+ Forget Search '[% searchname FILTER html %]'</a>
</td>
[% ELSE %]
<td> </td>