Commit 1eff1bd3 authored by Winnie Hellmann's avatar Winnie Hellmann

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
parents 689bc9ea f71e48a0
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 10.2.3 (2017-11-30)
### Fixed (7 changes) ### Fixed (7 changes)
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
...@@ -85,7 +86,7 @@ ...@@ -85,7 +86,7 @@
}; };
this.isRequesting = true; this.isRequesting = true;
this.oldContent = this.note.note_html; this.oldContent = this.note.note_html;
this.note.note_html = noteText; this.note.note_html = escape(noteText);
this.updateNote(data) this.updateNote(data)
.then(() => { .then(() => {
......
...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -272,7 +272,7 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo? render 'projects/empty' if @project.empty_repo?
else else
if @project.wiki_enabled? if can?(current_user, :read_wiki, @project)
@project_wiki = @project.wiki @project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id]) @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
......
...@@ -58,7 +58,7 @@ module PreferencesHelper ...@@ -58,7 +58,7 @@ module PreferencesHelper
user_view user_view
elsif user_view == "activity" elsif user_view == "activity"
"activity" "activity"
elsif @project.wiki_enabled? elsif can?(current_user, :read_wiki, @project)
"wiki" "wiki"
elsif @project.feature_available?(:issues, current_user) elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues" "projects/issues/issues"
......
...@@ -315,6 +315,8 @@ class User < ActiveRecord::Base ...@@ -315,6 +315,8 @@ class User < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
query = query.downcase
order = <<~SQL order = <<~SQL
CASE CASE
WHEN users.name = %{query} THEN 0 WHEN users.name = %{query} THEN 0
...@@ -324,8 +326,11 @@ class User < ActiveRecord::Base ...@@ -324,8 +326,11 @@ class User < ActiveRecord::Base
END END
SQL SQL
fuzzy_search(query, [:name, :email, :username]) where(
.reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) 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 end
# searches user by given pattern # searches user by given pattern
...@@ -333,15 +338,17 @@ class User < ActiveRecord::Base ...@@ -333,15 +338,17 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query) def search_with_secondary_emails(query)
query = query.downcase
email_table = Email.arel_table email_table = Email.arel_table
matched_by_emails_user_ids = email_table matched_by_emails_user_ids = email_table
.project(email_table[:user_id]) .project(email_table[:user_id])
.where(Email.fuzzy_arel_match(:email, query)) .where(email_table[:email].eq(query))
where( where(
fuzzy_arel_match(:name, query) fuzzy_arel_match(:name, query)
.or(fuzzy_arel_match(:email, query))
.or(fuzzy_arel_match(:username, query)) .or(fuzzy_arel_match(:username, query))
.or(arel_table[:email].eq(query))
.or(arel_table[:id].in(matched_by_emails_user_ids)) .or(arel_table[:id].in(matched_by_emails_user_ids))
) )
end end
......
...@@ -248,8 +248,21 @@ module API ...@@ -248,8 +248,21 @@ module API
end end
class GroupDetail < Group class GroupDetail < Group
expose :projects, using: Entities::Project expose :projects, using: Entities::Project do |group, options|
expose :shared_projects, using: Entities::Project 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 end
class Commit < Grape::Entity class Commit < Grape::Entity
......
...@@ -161,6 +161,8 @@ module API ...@@ -161,6 +161,8 @@ module API
use :issue_params use :issue_params
end end
post ':id/issues' do post ':id/issues' do
authorize! :create_issue, user_project
# Setting created_at time only allowed for admins and project owners # Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at) params.delete(:created_at)
......
...@@ -58,6 +58,10 @@ FactoryGirl.define do ...@@ -58,6 +58,10 @@ FactoryGirl.define do
end end
end end
trait :readme do
project_view :readme
end
factory :omniauth_user do factory :omniauth_user do
transient do transient do
extern_uid '123456' extern_uid '123456'
......
...@@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do ...@@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do
end end
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 scenario 'remove user from group', :js do
group.add_owner(user1) group.add_owner(user1)
group.add_developer(user2) group.add_developer(user2)
......
...@@ -77,15 +77,6 @@ describe PreferencesHelper do ...@@ -77,15 +77,6 @@ describe PreferencesHelper do
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
describe '#default_project_view' do describe '#default_project_view' do
context 'user not signed in' do context 'user not signed in' do
before do before do
...@@ -125,5 +116,70 @@ describe PreferencesHelper do ...@@ -125,5 +116,70 @@ describe PreferencesHelper do
end end
end 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
end end
...@@ -41,4 +41,19 @@ describe('issue_note', () => { ...@@ -41,4 +41,19 @@ describe('issue_note', () => {
it('should render issue body', () => { it('should render issue body', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); 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 ...@@ -913,11 +913,11 @@ describe User do
describe 'email matching' do describe 'email matching' do
it 'returns users with a matching Email' 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 end
it 'returns users with a partially matching Email' do it 'does not return users with a partially matching Email' do
expect(described_class.search(user.email[0..2])).to eq([user, user2]) expect(described_class.search(user.email[0..2])).not_to include(user, user2)
end end
it 'returns users with a matching Email regardless of the casing' do it 'returns users with a matching Email regardless of the casing' do
...@@ -973,8 +973,8 @@ describe User do ...@@ -973,8 +973,8 @@ describe User do
expect(search_with_secondary_emails(user.email)).to eq([user]) expect(search_with_secondary_emails(user.email)).to eq([user])
end end
it 'returns users with a partially matching email' do it 'does not return users with a partially matching email' do
expect(search_with_secondary_emails(user.email[0..2])).to eq([user]) expect(search_with_secondary_emails(user.email[0..2])).not_to include([user])
end end
it 'returns users with a matching email regardless of the casing' do it 'returns users with a matching email regardless of the casing' do
...@@ -997,29 +997,8 @@ describe User do ...@@ -997,29 +997,8 @@ describe User do
expect(search_with_secondary_emails(email.email)).to eq([email.user]) expect(search_with_secondary_emails(email.email)).to eq([email.user])
end end
it 'returns users with a matching part of secondary email' do it 'does not return users with a matching part of secondary email' do
expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user]) expect(search_with_secondary_emails(email.email[1..4])).not_to include([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)
end end
end end
......
...@@ -173,6 +173,28 @@ describe API::Groups do ...@@ -173,6 +173,28 @@ describe API::Groups do
end end
describe "GET /groups/:id" do 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 context 'when unauthenticated' do
it 'returns 404 for a private group' do it 'returns 404 for a private group' do
get api("/groups/#{group2.id}") get api("/groups/#{group2.id}")
...@@ -183,6 +205,26 @@ describe API::Groups do ...@@ -183,6 +205,26 @@ describe API::Groups do
get api("/groups/#{group1.id}") get api("/groups/#{group1.id}")
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
end 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 end
context "when authenticated as user" do context "when authenticated as user" do
...@@ -222,6 +264,26 @@ describe API::Groups do ...@@ -222,6 +264,26 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end 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 end
context "when authenticated as admin" do context "when authenticated as admin" do
......
...@@ -860,6 +860,20 @@ describe API::Issues, :mailer do ...@@ -860,6 +860,20 @@ describe API::Issues, :mailer do
end end
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 it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user), post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3, title: 'new issue', labels: 'label, label2', weight: 3,
......
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