]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 466748: [SECURITY] Shared/saved searches can be deleted without user confirmation...
authorlpsolit%gmail.com <>
Mon, 2 Feb 2009 18:51:52 +0000 (18:51 +0000)
committerlpsolit%gmail.com <>
Mon, 2 Feb 2009 18:51:52 +0000 (18:51 +0000)
Bugzilla/Install/Localconfig.pm
Bugzilla/Template.pm
Bugzilla/Token.pm
buglist.cgi
template/en/default/account/prefs/saved-searches.html.tmpl
template/en/default/global/confirm-action.html.tmpl [new file with mode: 0644]
template/en/default/global/user-error.html.tmpl
template/en/default/list/list.html.tmpl

index ed502d8a771c886a97acda3bb25d4e9a57527e59..ce94f5b838377b728c4aadcad27da4dc3331d587 100644 (file)
@@ -31,6 +31,7 @@ package Bugzilla::Install::Localconfig;
 use strict;
 
 use Bugzilla::Constants;
+use Bugzilla::Util qw(generate_random_password);
 
 use Data::Dumper;
 use IO::File;
@@ -181,6 +182,17 @@ EOT
         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
     },
 );
index 53e23c5c35c0489cfb81da3c785348e784e89922..8c14da0a26c7e2d1c7980f414b88b77c18faaade 100644 (file)
@@ -39,12 +39,13 @@ use Bugzilla::Install::Requirements;
 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);
@@ -822,6 +823,9 @@ sub create {
                 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 => 
index 62aeb308696d0f93744c570fa315454c97ba4a36..f26f623c0a65a27a2cc4fcce52dd7f9726f484cc 100644 (file)
@@ -39,10 +39,12 @@ use Bugzilla::User;
 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
@@ -171,6 +173,53 @@ sub issue_session_token {
     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');
index ca693dfb3aeee8379c7ef389957dd51206f77c1c..afe16ae4a6e9a84ed96912f39e60d14cce4bdc0f 100755 (executable)
@@ -46,6 +46,7 @@ use Bugzilla::Bug;
 use Bugzilla::Product;
 use Bugzilla::Keyword;
 use Bugzilla::Field;
+use Bugzilla::Token;
 
 use Date::Parse;
 
@@ -273,7 +274,7 @@ sub LookupNamedQuery {
     $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
@@ -405,14 +406,16 @@ if ($cgi->param('cmdtype') eq "dorem" && $cgi->param('remaction') =~ /^run/) {
 # 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;
@@ -461,6 +464,10 @@ if ($cgi->param('cmdtype') eq "dorem") {
             # 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);
@@ -514,9 +521,12 @@ elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
             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;
             }
index 4beb0e82318d2625a314505e56945c88878b618f..38e787c8df4f208cc16759615d80a0b50258d476 100644 (file)
             Remove from <a href="editwhines.cgi">whining</a> first
           [% ELSE %]
             <a href="buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
-                     [% q.name FILTER url_quote %]">Forget</a>
+                     [% q.name FILTER url_quote %]&amp;token=
+                     [% issue_hash_token([q.id, q.name]) FILTER url_quote %]">Forget</a>
           [% END %]
         </td>
         <td align="center">
diff --git a/template/en/default/global/confirm-action.html.tmpl b/template/en/default/global/confirm-action.html.tmpl
new file mode 100644 (file)
index 0000000..998551d
--- /dev/null
@@ -0,0 +1,64 @@
+[%# 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 %]
index 1ded51102159f79b28b3d37d37158c24c1f1720a..e1a4bcf910a244cde703cae2dc325fb03c969e4f 100644 (file)
     The name <em>[% name FILTER html %]</em> is already used by another
     saved search. You first have to
     <a href="buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
-    [%- name FILTER url_quote %]">delete</a> it if you really want to use
-    this name.
+    [%- name FILTER url_quote %]&amp;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" %]
index a12c39a9ec04f49d0ba012e3480da559a895e983..757bb4787dadfde85578e549f8e220c01999d425 100644 (file)
       <td valign="middle" nowrap="nowrap">
         |
         <a href="buglist.cgi?cmdtype=dorem&amp;remaction=forget&amp;namedcmd=
-                [% searchname FILTER url_quote %]">Forget&nbsp;Search&nbsp;'
-                [% searchname FILTER html %]'</a>
+                [% searchname FILTER url_quote %]&amp;token=
+                [% issue_hash_token([search_id, searchname]) FILTER url_quote %]">
+          Forget&nbsp;Search&nbsp;'[% searchname FILTER html %]'</a>
       </td>
     [% ELSE %]
       <td>&nbsp;</td>