Merge branch 'mk-pick-10-2-4-security-fixes' into 'master'
Pick 10.2.4 security fixes into master See merge request gitlab-org/gitlab-ce!15821
This commit is contained in:
commit
1eff1bd385
14 changed files with 231 additions and 46 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -2,6 +2,17 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 10.2.4 (2017-12-08)
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- Fix e-mail address disclosure through member search fields
|
||||
- Prevent creating issues through API when user does not have permissions
|
||||
- Prevent an information disclosure in the Groups API
|
||||
- Fix user without access to private Wiki being able to see it on the project page
|
||||
- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment
|
||||
|
||||
|
||||
## 10.2.3 (2017-11-30)
|
||||
|
||||
### Fixed (7 changes)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { escape } from 'underscore';
|
||||
import Flash from '../../flash';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import noteHeader from './note_header.vue';
|
||||
|
@ -85,7 +86,7 @@
|
|||
};
|
||||
this.isRequesting = true;
|
||||
this.oldContent = this.note.note_html;
|
||||
this.note.note_html = noteText;
|
||||
this.note.note_html = escape(noteText);
|
||||
|
||||
this.updateNote(data)
|
||||
.then(() => {
|
||||
|
|
|
@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
|
||||
render 'projects/empty' if @project.empty_repo?
|
||||
else
|
||||
if @project.wiki_enabled?
|
||||
if can?(current_user, :read_wiki, @project)
|
||||
@project_wiki = @project.wiki
|
||||
@wiki_home = @project_wiki.find_page('home', params[:version_id])
|
||||
elsif @project.feature_available?(:issues, current_user)
|
||||
|
|
|
@ -58,7 +58,7 @@ module PreferencesHelper
|
|||
user_view
|
||||
elsif user_view == "activity"
|
||||
"activity"
|
||||
elsif @project.wiki_enabled?
|
||||
elsif can?(current_user, :read_wiki, @project)
|
||||
"wiki"
|
||||
elsif @project.feature_available?(:issues, current_user)
|
||||
"projects/issues/issues"
|
||||
|
|
|
@ -315,6 +315,8 @@ class User < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
query = query.downcase
|
||||
|
||||
order = <<~SQL
|
||||
CASE
|
||||
WHEN users.name = %{query} THEN 0
|
||||
|
@ -324,8 +326,11 @@ class User < ActiveRecord::Base
|
|||
END
|
||||
SQL
|
||||
|
||||
fuzzy_search(query, [:name, :email, :username])
|
||||
.reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
|
||||
where(
|
||||
fuzzy_arel_match(:name, query)
|
||||
.or(fuzzy_arel_match(:username, query))
|
||||
.or(arel_table[:email].eq(query))
|
||||
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
|
||||
end
|
||||
|
||||
# searches user by given pattern
|
||||
|
@ -333,15 +338,17 @@ class User < ActiveRecord::Base
|
|||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
|
||||
def search_with_secondary_emails(query)
|
||||
query = query.downcase
|
||||
|
||||
email_table = Email.arel_table
|
||||
matched_by_emails_user_ids = email_table
|
||||
.project(email_table[:user_id])
|
||||
.where(Email.fuzzy_arel_match(:email, query))
|
||||
.where(email_table[:email].eq(query))
|
||||
|
||||
where(
|
||||
fuzzy_arel_match(:name, query)
|
||||
.or(fuzzy_arel_match(:email, query))
|
||||
.or(fuzzy_arel_match(:username, query))
|
||||
.or(arel_table[:email].eq(query))
|
||||
.or(arel_table[:id].in(matched_by_emails_user_ids))
|
||||
)
|
||||
end
|
||||
|
|
|
@ -248,8 +248,21 @@ module API
|
|||
end
|
||||
|
||||
class GroupDetail < Group
|
||||
expose :projects, using: Entities::Project
|
||||
expose :shared_projects, using: Entities::Project
|
||||
expose :projects, using: Entities::Project do |group, options|
|
||||
GroupProjectsFinder.new(
|
||||
group: group,
|
||||
current_user: options[:current_user],
|
||||
options: { only_owned: true }
|
||||
).execute
|
||||
end
|
||||
|
||||
expose :shared_projects, using: Entities::Project do |group, options|
|
||||
GroupProjectsFinder.new(
|
||||
group: group,
|
||||
current_user: options[:current_user],
|
||||
options: { only_shared: true }
|
||||
).execute
|
||||
end
|
||||
end
|
||||
|
||||
class Commit < Grape::Entity
|
||||
|
|
|
@ -161,6 +161,8 @@ module API
|
|||
use :issue_params
|
||||
end
|
||||
post ':id/issues' do
|
||||
authorize! :create_issue, user_project
|
||||
|
||||
# Setting created_at time only allowed for admins and project owners
|
||||
unless current_user.admin? || user_project.owner == current_user
|
||||
params.delete(:created_at)
|
||||
|
|
|
@ -58,6 +58,10 @@ FactoryGirl.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :readme do
|
||||
project_view :readme
|
||||
end
|
||||
|
||||
factory :omniauth_user do
|
||||
transient do
|
||||
extern_uid '123456'
|
||||
|
|
|
@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do
|
|||
end
|
||||
end
|
||||
|
||||
scenario 'do not disclose email addresses', :js do
|
||||
group.add_owner(user1)
|
||||
create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe")
|
||||
|
||||
visit group_group_members_path(group)
|
||||
|
||||
find('.select2-container').click
|
||||
select_input = find('.select2-input')
|
||||
|
||||
select_input.send_keys('@gitlab.com')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('No matches found')
|
||||
|
||||
select_input.native.clear
|
||||
select_input.send_keys('undisclosed_email@gitlab.com')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content("Jane 'invisible' Doe")
|
||||
end
|
||||
|
||||
scenario 'remove user from group', :js do
|
||||
group.add_owner(user1)
|
||||
group.add_developer(user2)
|
||||
|
|
|
@ -77,15 +77,6 @@ describe PreferencesHelper do
|
|||
end
|
||||
end
|
||||
|
||||
def stub_user(messages = {})
|
||||
if messages.empty?
|
||||
allow(helper).to receive(:current_user).and_return(nil)
|
||||
else
|
||||
allow(helper).to receive(:current_user)
|
||||
.and_return(double('user', messages))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#default_project_view' do
|
||||
context 'user not signed in' do
|
||||
before do
|
||||
|
@ -125,5 +116,70 @@ describe PreferencesHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user signed in' do
|
||||
let(:user) { create(:user, :readme) }
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
|
||||
before do
|
||||
helper.instance_variable_set(:@project, project)
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
context 'when the user is allowed to see the code' do
|
||||
it 'returns the project view' do
|
||||
allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
|
||||
|
||||
expect(helper.default_project_view).to eq('readme')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with wikis enabled and the right policy for the user' do
|
||||
before do
|
||||
project.project_feature.update_attribute(:issues_access_level, 0)
|
||||
allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
|
||||
end
|
||||
|
||||
it 'returns wiki if the user has the right policy' do
|
||||
allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true)
|
||||
|
||||
expect(helper.default_project_view).to eq('wiki')
|
||||
end
|
||||
|
||||
it 'returns customize_workflow if the user does not have the right policy' do
|
||||
allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
|
||||
|
||||
expect(helper.default_project_view).to eq('customize_workflow')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with issues as a feature available' do
|
||||
it 'return issues' do
|
||||
allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
|
||||
allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
|
||||
|
||||
expect(helper.default_project_view).to eq('projects/issues/issues')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no activity, no wikies and no issues' do
|
||||
it 'returns customize_workflow as default' do
|
||||
project.project_feature.update_attribute(:issues_access_level, 0)
|
||||
allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
|
||||
allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
|
||||
|
||||
expect(helper.default_project_view).to eq('customize_workflow')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def stub_user(messages = {})
|
||||
if messages.empty?
|
||||
allow(helper).to receive(:current_user).and_return(nil)
|
||||
else
|
||||
allow(helper).to receive(:current_user)
|
||||
.and_return(double('user', messages))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,4 +41,19 @@ describe('issue_note', () => {
|
|||
it('should render issue body', () => {
|
||||
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
|
||||
});
|
||||
|
||||
it('prevents note preview xss', (done) => {
|
||||
const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
|
||||
const alertSpy = spyOn(window, 'alert');
|
||||
vm.updateNote = () => new Promise($.noop);
|
||||
|
||||
vm.formUpdateHandler(noteBody, null, $.noop);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(alertSpy).not.toHaveBeenCalled();
|
||||
expect(vm.note.note_html).toEqual(_.escape(noteBody));
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -913,11 +913,11 @@ describe User do
|
|||
|
||||
describe 'email matching' do
|
||||
it 'returns users with a matching Email' do
|
||||
expect(described_class.search(user.email)).to eq([user, user2])
|
||||
expect(described_class.search(user.email)).to eq([user])
|
||||
end
|
||||
|
||||
it 'returns users with a partially matching Email' do
|
||||
expect(described_class.search(user.email[0..2])).to eq([user, user2])
|
||||
it 'does not return users with a partially matching Email' do
|
||||
expect(described_class.search(user.email[0..2])).not_to include(user, user2)
|
||||
end
|
||||
|
||||
it 'returns users with a matching Email regardless of the casing' do
|
||||
|
@ -973,8 +973,8 @@ describe User do
|
|||
expect(search_with_secondary_emails(user.email)).to eq([user])
|
||||
end
|
||||
|
||||
it 'returns users with a partially matching email' do
|
||||
expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
|
||||
it 'does not return users with a partially matching email' do
|
||||
expect(search_with_secondary_emails(user.email[0..2])).not_to include([user])
|
||||
end
|
||||
|
||||
it 'returns users with a matching email regardless of the casing' do
|
||||
|
@ -997,29 +997,8 @@ describe User do
|
|||
expect(search_with_secondary_emails(email.email)).to eq([email.user])
|
||||
end
|
||||
|
||||
it 'returns users with a matching part of secondary email' do
|
||||
expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
|
||||
end
|
||||
|
||||
it 'return users with a matching part of secondary email regardless of case' do
|
||||
expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
|
||||
expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
|
||||
expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
|
||||
end
|
||||
|
||||
it 'returns multiple users with matching secondary emails' do
|
||||
email1 = create(:email, email: '1_testemail@example.com')
|
||||
email2 = create(:email, email: '2_testemail@example.com')
|
||||
email3 = create(:email, email: 'other@email.com')
|
||||
email3.user.update_attributes!(email: 'another@mail.com')
|
||||
|
||||
expect(
|
||||
search_with_secondary_emails('testemail@example.com').map(&:id)
|
||||
).to include(email1.user.id, email2.user.id)
|
||||
|
||||
expect(
|
||||
search_with_secondary_emails('testemail@example.com').map(&:id)
|
||||
).not_to include(email3.user.id)
|
||||
it 'does not return users with a matching part of secondary email' do
|
||||
expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -173,6 +173,28 @@ describe API::Groups do
|
|||
end
|
||||
|
||||
describe "GET /groups/:id" do
|
||||
# Given a group, create one project for each visibility level
|
||||
#
|
||||
# group - Group to add projects to
|
||||
# share_with - If provided, each project will be shared with this Group
|
||||
#
|
||||
# Returns a Hash of visibility_level => Project pairs
|
||||
def add_projects_to_group(group, share_with: nil)
|
||||
projects = {
|
||||
public: create(:project, :public, namespace: group),
|
||||
internal: create(:project, :internal, namespace: group),
|
||||
private: create(:project, :private, namespace: group)
|
||||
}
|
||||
|
||||
if share_with
|
||||
create(:project_group_link, project: projects[:public], group: share_with)
|
||||
create(:project_group_link, project: projects[:internal], group: share_with)
|
||||
create(:project_group_link, project: projects[:private], group: share_with)
|
||||
end
|
||||
|
||||
projects
|
||||
end
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns 404 for a private group' do
|
||||
get api("/groups/#{group2.id}")
|
||||
|
@ -183,6 +205,26 @@ describe API::Groups do
|
|||
get api("/groups/#{group1.id}")
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
end
|
||||
|
||||
it 'returns only public projects in the group' do
|
||||
public_group = create(:group, :public)
|
||||
projects = add_projects_to_group(public_group)
|
||||
|
||||
get api("/groups/#{public_group.id}")
|
||||
|
||||
expect(json_response['projects'].map { |p| p['id'].to_i })
|
||||
.to contain_exactly(projects[:public].id)
|
||||
end
|
||||
|
||||
it 'returns only public projects shared with the group' do
|
||||
public_group = create(:group, :public)
|
||||
projects = add_projects_to_group(public_group, share_with: group1)
|
||||
|
||||
get api("/groups/#{group1.id}")
|
||||
|
||||
expect(json_response['shared_projects'].map { |p| p['id'].to_i })
|
||||
.to contain_exactly(projects[:public].id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when authenticated as user" do
|
||||
|
@ -222,6 +264,26 @@ describe API::Groups do
|
|||
|
||||
expect(response).to have_gitlab_http_status(404)
|
||||
end
|
||||
|
||||
it 'returns only public and internal projects in the group' do
|
||||
public_group = create(:group, :public)
|
||||
projects = add_projects_to_group(public_group)
|
||||
|
||||
get api("/groups/#{public_group.id}", user2)
|
||||
|
||||
expect(json_response['projects'].map { |p| p['id'].to_i })
|
||||
.to contain_exactly(projects[:public].id, projects[:internal].id)
|
||||
end
|
||||
|
||||
it 'returns only public and internal projects shared with the group' do
|
||||
public_group = create(:group, :public)
|
||||
projects = add_projects_to_group(public_group, share_with: group1)
|
||||
|
||||
get api("/groups/#{group1.id}", user2)
|
||||
|
||||
expect(json_response['shared_projects'].map { |p| p['id'].to_i })
|
||||
.to contain_exactly(projects[:public].id, projects[:internal].id)
|
||||
end
|
||||
end
|
||||
|
||||
context "when authenticated as admin" do
|
||||
|
|
|
@ -860,6 +860,20 @@ describe API::Issues, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
context 'user does not have permissions to create issue' do
|
||||
let(:not_member) { create(:user) }
|
||||
|
||||
before do
|
||||
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
it 'renders 403' do
|
||||
post api("/projects/#{project.id}/issues", not_member), title: 'new issue'
|
||||
|
||||
expect(response).to have_gitlab_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new project issue' do
|
||||
post api("/projects/#{project.id}/issues", user),
|
||||
title: 'new issue', labels: 'label, label2', weight: 3,
|
||||
|
|
Loading…
Reference in a new issue