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:
Winnie Hellmann 2017-12-11 12:07:57 +00:00
commit 1eff1bd385
14 changed files with 231 additions and 46 deletions

View file

@ -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)

View file

@ -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(() => {

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -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);
});
});

View file

@ -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

View file

@ -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

View file

@ -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,