Commit aec3475d authored by Rémy Coutable's avatar Rémy Coutable

Fix an information disclosure when requesting access to a group containing private projects

The issue was with the `User#groups` and `User#projects` associations
which goes through the `User#group_members` and `User#project_members`.

Initially I chose to use a secure approach by storing the requester's
user ID in `Member#created_by_id` instead of `Member#user_id` because I
was aware that there was a security risk since I didn't know the
codebase well enough.

Then during the review, we decided to change that and directly store the
requester's user ID into `Member#user_id` (for the sake of simplifying
the code I believe), meaning that every `group_members` / `project_members`
association would include the requesters by default...

My bad for not checking that all the `group_members` / `project_members`
associations and the ones that go through them (e.g. `Group#users` and
`Project#users`) were made safe with the `where(requested_at: nil)` /
`where(members: { requested_at: nil })` scopes.

Now they are all secure.
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 4477dc24
class Dashboard::GroupsController < Dashboard::ApplicationController class Dashboard::GroupsController < Dashboard::ApplicationController
def index def index
@group_members = current_user.group_members.page(params[:page]) @group_members = current_user.group_members.includes(:source).page(params[:page])
end end
end end
...@@ -11,7 +11,7 @@ class Group < Namespace ...@@ -11,7 +11,7 @@ class Group < Namespace
has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members has_many :users, -> { where(members: { requested_at: nil }) }, through: :group_members
has_many :owners, has_many :owners,
-> { where(members: { access_level: Gitlab::Access::OWNER }) }, -> { where(members: { requested_at: nil, access_level: Gitlab::Access::OWNER }) },
through: :group_members, through: :group_members,
source: :user source: :user
......
...@@ -57,7 +57,7 @@ class User < ActiveRecord::Base ...@@ -57,7 +57,7 @@ class User < ActiveRecord::Base
# Groups # Groups
has_many :members, dependent: :destroy has_many :members, dependent: :destroy
has_many :group_members, dependent: :destroy, source: 'GroupMember' has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember'
has_many :groups, through: :group_members has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
...@@ -65,7 +65,7 @@ class User < ActiveRecord::Base ...@@ -65,7 +65,7 @@ class User < ActiveRecord::Base
# Projects # Projects
has_many :groups_projects, through: :groups, source: :projects has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, dependent: :destroy, class_name: 'ProjectMember' has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember'
has_many :projects, through: :project_members has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
......
- page_title "Groups", @user.name, "Users" - page_title "Groups", @user.name, "Users"
= render 'admin/users/head' = render 'admin/users/head'
- if @user.group_members.present? - group_members = @user.group_members.includes(:source)
- if group_members.any?
.panel.panel-default .panel.panel-default
.panel-heading Groups: .panel-heading Groups:
%ul.well-list %ul.well-list
- @user.group_members.each do |group_member| - group_members.each do |group_member|
- group = group_member.group - group = group_member.group
%li.group_member %li.group_member
%span{class: ("list-item-name" unless group_member.owner?)} %span{class: ("list-item-name" unless group_member.owner?)}
......
...@@ -4,6 +4,7 @@ feature 'Groups > Members > User requests access', feature: true do ...@@ -4,6 +4,7 @@ feature 'Groups > Members > User requests access', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:owner) { create(:user) } let(:owner) { create(:user) }
let(:group) { create(:group, :public) } let(:group) { create(:group, :public) }
let!(:project) { create(:project, :private, namespace: group) }
background do background do
group.add_owner(owner) group.add_owner(owner)
...@@ -24,6 +25,20 @@ feature 'Groups > Members > User requests access', feature: true do ...@@ -24,6 +25,20 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).not_to have_content 'Leave Group' expect(page).not_to have_content 'Leave Group'
end end
scenario 'user does not see private projects' do
perform_enqueued_jobs { click_link 'Request Access' }
expect(page).not_to have_content project.name
end
scenario 'user does not see group in the Dashboard > Groups page' do
perform_enqueued_jobs { click_link 'Request Access' }
visit dashboard_groups_path
expect(page).not_to have_content group.name
end
scenario 'user is not listed in the group members page' do scenario 'user is not listed in the group members page' do
click_link 'Request Access' click_link 'Request Access'
......
...@@ -31,6 +31,26 @@ describe User, models: true do ...@@ -31,6 +31,26 @@ describe User, models: true do
it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) } it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
group = create(:group, :public)
group.request_access(user)
expect(user.group_members).to be_empty
end
end
describe '#project_members' do
it 'does not include project memberships for which user is a requester' do
user = create(:user)
project = create(:project, :public)
project.request_access(user)
expect(user.project_members).to be_empty
end
end
end end
describe 'validations' do describe 'validations' do
......
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