Commit 5d70d16f authored by Frédéric Buclin's avatar Frédéric Buclin

Bug 653477: (CVE-2011-2380) [SECURITY] Group names can be guessed when creating or editing a bug

r=mkanat a=LpSolit
parent b9c01561
...@@ -1622,6 +1622,8 @@ sub _check_dup_id { ...@@ -1622,6 +1622,8 @@ sub _check_dup_id {
sub _check_groups { sub _check_groups {
my ($invocant, $group_names, undef, $params) = @_; my ($invocant, $group_names, undef, $params) = @_;
my $bug_id = blessed($invocant) ? $invocant->id : undef;
my $product = blessed($invocant) ? $invocant->product_obj my $product = blessed($invocant) ? $invocant->product_obj
: $params->{product}; : $params->{product};
my %add_groups; my %add_groups;
...@@ -1640,14 +1642,12 @@ sub _check_groups { ...@@ -1640,14 +1642,12 @@ sub _check_groups {
if !ref $group_names; if !ref $group_names;
# First check all the groups they chose to set. # First check all the groups they chose to set.
my %args = ( product => $product->name, bug_id => $bug_id, action => 'add' );
foreach my $name (@$group_names) { foreach my $name (@$group_names) {
my $group = Bugzilla::Group->check( my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name });
{ name => $name, product => $product,
_error => 'group_restriction_not_allowed' });
if (!$product->group_is_settable($group)) { if (!$product->group_is_settable($group)) {
ThrowUserError('group_restriction_not_allowed', ThrowUserError('group_restriction_not_allowed', { %args, name => $name });
{ name => $name, product => $product });
} }
$add_groups{$group->id} = $group; $add_groups{$group->id} = $group;
} }
...@@ -2512,20 +2512,18 @@ sub _set_product { ...@@ -2512,20 +2512,18 @@ sub _set_product {
$self->set_target_milestone($tm_name); $self->set_target_milestone($tm_name);
} }
} }
if ($product_changed) { if ($product_changed) {
# Remove groups that can't be set in the new product, or that aren't # Remove groups that can't be set in the new product.
# active.
#
# We copy this array because the original array is modified while we're # We copy this array because the original array is modified while we're
# working, and that confuses "foreach". # working, and that confuses "foreach".
my @current_groups = @{$self->groups_in}; my @current_groups = @{$self->groups_in};
foreach my $group (@current_groups) { foreach my $group (@current_groups) {
if (!$group->is_active or !$product->group_is_valid($group)) { if (!$product->group_is_valid($group)) {
$self->remove_group($group); $self->remove_group($group);
} }
} }
# Make sure the bug is in all the mandatory groups for the new product. # Make sure the bug is in all the mandatory groups for the new product.
foreach my $group (@{$product->groups_mandatory}) { foreach my $group (@{$product->groups_mandatory}) {
$self->add_group($group); $self->add_group($group);
...@@ -2757,18 +2755,25 @@ sub modify_keywords { ...@@ -2757,18 +2755,25 @@ sub modify_keywords {
sub add_group { sub add_group {
my ($self, $group) = @_; my ($self, $group) = @_;
# Invalid ids are silently ignored. (We can't tell people whether
# or not a group exists.)
$group = Bugzilla::Group->check($group) if !blessed $group;
return if !$group->is_active or !$group->is_bug_group; # If the user enters "FoO" but the DB has "Foo", $group->name would
# return "Foo" and thus revealing the existence of the group name.
# So we have to store and pass the name as entered by the user to
# the error message, if we have it.
my $group_name = blessed($group) ? $group->name : $group;
my $args = { name => $group_name, product => $self->product,
bug_id => $self->id, action => 'add' };
$group = Bugzilla::Group->check_no_disclose($args) if !blessed $group;
# If the bug is already in this group, then there is nothing to do.
return if $self->in_group($group);
# Make sure that bugs in this product can actually be restricted # Make sure that bugs in this product can actually be restricted
# to this group by the current user. # to this group by the current user.
$self->product_obj->group_is_settable($group) $self->product_obj->group_is_settable($group)
|| ThrowUserError('group_invalid_restriction', || ThrowUserError('group_restriction_not_allowed', $args);
{ product => $self->product, group => $group,
bug => $self });
# OtherControl people can add groups only during a product change, # OtherControl people can add groups only during a product change,
# and only when the group is not NA for them. # and only when the group is not NA for them.
...@@ -2777,37 +2782,38 @@ sub add_group { ...@@ -2777,37 +2782,38 @@ sub add_group {
if (!$self->{_old_product_name} if (!$self->{_old_product_name}
|| $controls->{othercontrol} == CONTROLMAPNA) || $controls->{othercontrol} == CONTROLMAPNA)
{ {
ThrowUserError('group_change_denied', ThrowUserError('group_restriction_not_allowed', $args);
{ bug => $self, group => $group });
} }
} }
my $current_groups = $self->groups_in; my $current_groups = $self->groups_in;
if (!grep($group->id == $_->id, @$current_groups)) { push(@$current_groups, $group);
push(@$current_groups, $group);
}
} }
sub remove_group { sub remove_group {
my ($self, $group) = @_; my ($self, $group) = @_;
$group = Bugzilla::Group->check($group) if !blessed $group;
# See add_group() for the reason why we store the user input.
# First, check if this is a valid group for this product. my $group_name = blessed($group) ? $group->name : $group;
# You can *always* remove a group that is not valid for this product my $args = { name => $group_name, product => $self->product,
# or that is not active, so we don't do any other checks if either of bug_id => $self->id, action => 'remove' };
# those are the case. (Users might remove inactive groups, and set_product
# removes groups that aren't valid for this product.) $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group;
#
# This particularly happens when isbuggroup is no longer 1, and we're # If the bug isn't in this group, then either the name is misspelled,
# moving a bug to a new product. # or the group really doesn't exist. Let the user know about this problem.
if ($group->is_active and $self->product_obj->group_is_valid($group)) { $self->in_group($group) || ThrowUserError('group_invalid_removal', $args);
# Check if this is a valid group for this product. You can *always*
# remove a group that is not valid for this product (set_product does this).
# This particularly happens when we're moving a bug to a new product.
# You still have to be a member of an inactive group to remove it.
if ($self->product_obj->group_is_valid($group)) {
my $controls = $self->product_obj->group_controls->{$group->id}; my $controls = $self->product_obj->group_controls->{$group->id};
# Nobody can ever remove a Mandatory group. # Nobody can ever remove a Mandatory group, unless it became inactive.
if ($controls->{membercontrol} == CONTROLMAPMANDATORY) { if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) {
ThrowUserError('group_invalid_removal', ThrowUserError('group_invalid_removal', $args);
{ product => $self->product, group => $group,
bug => $self });
} }
# OtherControl people can remove groups only during a product change, # OtherControl people can remove groups only during a product change,
...@@ -2817,12 +2823,11 @@ sub remove_group { ...@@ -2817,12 +2823,11 @@ sub remove_group {
|| $controls->{othercontrol} == CONTROLMAPMANDATORY || $controls->{othercontrol} == CONTROLMAPMANDATORY
|| $controls->{othercontrol} == CONTROLMAPNA) || $controls->{othercontrol} == CONTROLMAPNA)
{ {
ThrowUserError('group_change_denied', ThrowUserError('group_invalid_removal', $args);
{ bug => $self, group => $group });
} }
} }
} }
my $current_groups = $self->groups_in; my $current_groups = $self->groups_in;
@$current_groups = grep { $_->id != $group->id } @$current_groups; @$current_groups = grep { $_->id != $group->id } @$current_groups;
} }
...@@ -3529,6 +3534,11 @@ sub groups_in { ...@@ -3529,6 +3534,11 @@ sub groups_in {
return $self->{'groups_in'}; return $self->{'groups_in'};
} }
sub in_group {
my ($self, $group) = @_;
return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0;
}
sub user { sub user {
my $self = shift; my $self = shift;
return $self->{'user'} if exists $self->{'user'}; return $self->{'user'} if exists $self->{'user'};
......
...@@ -439,6 +439,21 @@ sub ValidateGroupName { ...@@ -439,6 +439,21 @@ sub ValidateGroupName {
return $ret; return $ret;
} }
sub check_no_disclose {
my ($class, $params) = @_;
my $action = delete $params->{action};
$action =~ /^(?:add|remove)$/
or ThrowCodeError('bad_arg', { argument => $action,
function => "${class}::check_no_disclose" });
$params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed'
: 'group_invalid_removal';
my $group = $class->check($params);
return $group;
}
############################### ###############################
### Validators ### ### Validators ###
############################### ###############################
...@@ -538,6 +553,47 @@ Returns: It returns the group id if successful ...@@ -538,6 +553,47 @@ Returns: It returns the group id if successful
=over =over
=item C<check_no_disclose>
=over
=item B<Description>
Throws an error if the user cannot add or remove this group to/from a given
bug, but doesn't specify if this is because the group doesn't exist, or the
user is not allowed to edit this group restriction.
=item B<Params>
This method takes a single hashref as argument, with the following keys:
=over
=item C<name>
C<string> The name of the group to add or remove.
=item C<bug_id>
C<integer> The ID of the bug to which the group change applies.
=item C<product>
C<string> The name of the product the bug belongs to.
=item C<action>
C<string> Must be either C<add> or C<remove>, depending on whether the group
must be added or removed from the bug. Any other value will generate an error.
=back
=item C<Returns>
A C<Bugzilla::Group> object on success, else an error is thrown.
=back
=item C<check_members_are_visible> =item C<check_members_are_visible>
Throws an error if this group is not visible (according to Throws an error if this group is not visible (according to
......
...@@ -680,10 +680,12 @@ sub groups_mandatory { ...@@ -680,10 +680,12 @@ sub groups_mandatory {
# if this group can be validly set by the currently-logged-in user. # if this group can be validly set by the currently-logged-in user.
sub group_is_settable { sub group_is_settable {
my ($self, $group) = @_; my ($self, $group) = @_;
my $group_id = blessed($group) ? $group->id : $group;
my $is_mandatory = grep { $group_id == $_->id } return 0 unless ($group->is_active && $group->is_bug_group);
my $is_mandatory = grep { $group->id == $_->id }
@{ $self->groups_mandatory }; @{ $self->groups_mandatory };
my $is_available = grep { $group_id == $_->id } my $is_available = grep { $group->id == $_->id }
@{ $self->groups_available }; @{ $self->groups_available };
return ($is_mandatory or $is_available) ? 1 : 0; return ($is_mandatory or $is_available) ? 1 : 0;
} }
...@@ -948,7 +950,7 @@ a bug. (In fact, the user I<must> set the Mandatory group on the bug.) ...@@ -948,7 +950,7 @@ a bug. (In fact, the user I<must> set the Mandatory group on the bug.)
=over =over
=item C<$group> - Either a numeric group id or a L<Bugzilla::Group> object. =item C<$group> - A L<Bugzilla::Group> object.
=back =back
......
...@@ -109,8 +109,7 @@ use constant WS_ERROR_CODE => { ...@@ -109,8 +109,7 @@ use constant WS_ERROR_CODE => {
dupe_loop_detected => 118, dupe_loop_detected => 118,
dupe_id_required => 119, dupe_id_required => 119,
# Bug-related group errors # Bug-related group errors
group_change_denied => 120, group_invalid_removal => 120,
group_invalid_restriction => 120,
group_restriction_not_allowed => 120, group_restriction_not_allowed => 120,
# Status/Resolution errors # Status/Resolution errors
missing_resolution => 121, missing_resolution => 121,
......
...@@ -345,7 +345,17 @@ foreach my $field (@custom_fields) { ...@@ -345,7 +345,17 @@ foreach my $field (@custom_fields) {
} }
} }
# We are going to alter the list of removed groups, so we keep a copy here.
my @unchecked_groups = @$removed_groups;
foreach my $b (@bug_objects) { foreach my $b (@bug_objects) {
# Don't blindly ask to remove unchecked groups available in the UI.
# A group can be already unchecked, and the user didn't try to remove it.
# In this case, we don't want remove_group() to complain.
my @remove_groups;
foreach my $g (@{$b->groups_in}) {
push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups;
}
local $set_all_fields{groups}->{remove} = \@remove_groups;
$b->set_all(\%set_all_fields); $b->set_all(\%set_all_fields);
} }
......
...@@ -735,12 +735,6 @@ ...@@ -735,12 +735,6 @@
in the database which refer to it. All references to this group must in the database which refer to it. All references to this group must
be removed before you can remove it. be removed before you can remove it.
[% ELSIF error == "group_change_denied" %]
[% title = "Cannot Add/Remove That Group" %]
You tried to add or remove the '[% group.name FILTER html %]' group
from [% terms.bug %] [%+ bug.id FILTER html %], but you do not
have permissions to do so.
[% ELSIF error == "group_exists" %] [% ELSIF error == "group_exists" %]
[% title = "The group already exists" %] [% title = "The group already exists" %]
The group [% name FILTER html %] already exists. The group [% name FILTER html %] already exists.
...@@ -761,23 +755,17 @@ ...@@ -761,23 +755,17 @@
[% ELSIF error == "group_invalid_removal" %] [% ELSIF error == "group_invalid_removal" %]
You tried to remove [% terms.bug %] [%+ bug.id FILTER html %] You tried to remove [% terms.bug %] [%+ bug_id FILTER html %]
from the '[% group.name FILTER html %]' group, but [% terms.bugs %] from the '[% name FILTER html %]' group, but either this group does not exist,
in the '[% product FILTER html %]' product can not be removed from that or you are not allowed to remove [% terms.bugs %] from this group in the
group. '[% product FILTER html %]' product.
[% ELSIF error == "group_invalid_restriction" %]
You tried to restrict [% terms.bug %] [%+ bug.id FILTER html %] to
to the '[% group.name FILTER html %]' group, but [% terms.bugs %] in the
'[% product FILTER html %]' product can not be restricted to
that group.
[% ELSIF error == "group_restriction_not_allowed" %] [% ELSIF error == "group_restriction_not_allowed" %]
[% title = "Group Restriction Not Allowed" %] [% title = "Group Restriction Not Allowed" %]
You tried to restrict [% terms.abug %] to the "[% name FILTER html %]" You tried to restrict [% bug_id ? "$terms.bug $bug_id" : terms.abug FILTER html %]
group, but either this group does not exist, or you are not allowed to the '[% name FILTER html %]' group, but either this group does not exist,
to restrict [% terms.bugs %] to this group in the "[% product.name FILTER html %]" or you are not allowed to restrict [% terms.bugs %] to this group in the
product. '[% product FILTER html %]' product.
[% ELSIF error == "group_not_specified" %] [% ELSIF error == "group_not_specified" %]
[% title = "Group not specified" %] [% title = "Group not specified" %]
......
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