Commit bd46c8ab authored by Jen-Shin Lin's avatar Jen-Shin Lin Committed by Stan Hu

Merge branch 'security-10-1' into '10-1-stable'

Security fixes for 10.1 RC

See merge request gitlab/gitlabhq!2209
parent 9978ef98
...@@ -123,8 +123,8 @@ class FilteredSearchVisualTokens { ...@@ -123,8 +123,8 @@ class FilteredSearchVisualTokens {
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue; tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = ` tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar"> <img class="avatar s20" src="${user.avatar_url}" alt="">
${user.name} ${_.escape(user.name)}
`; `;
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
}) })
......
...@@ -2,7 +2,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -2,7 +2,6 @@ class Projects::ApplicationController < ApplicationController
include RoutableActions include RoutableActions
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :redirect_git_extension
before_action :project before_action :project
before_action :repository before_action :repository
layout 'project' layout 'project'
...@@ -11,15 +10,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -11,15 +10,6 @@ class Projects::ApplicationController < ApplicationController
private private
def redirect_git_extension
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
redirect_to url_for(params.merge(format: nil)) if params[:format] == 'git'
end
def project def project
return @project if @project return @project if @project
return nil unless params[:project_id] || params[:id] return nil unless params[:project_id] || params[:id]
......
...@@ -4,6 +4,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
before_action :repository, except: [:index, :new, :create] before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :assign_ref_vars, only: [:show], if: :repo_exists?
...@@ -389,4 +390,13 @@ class ProjectsController < Projects::ApplicationController ...@@ -389,4 +390,13 @@ class ProjectsController < Projects::ApplicationController
def project_export_enabled def project_export_enabled
render_404 unless current_application_settings.project_export_enabled? render_404 unless current_application_settings.project_export_enabled?
end end
def redirect_git_extension
# Redirect from
# localhost/group/project.git
# to
# localhost/group/project
#
redirect_to request.original_url.sub(/\.git\/?\Z/, '') if params[:format] == 'git'
end
end end
...@@ -7,6 +7,8 @@ module Storage ...@@ -7,6 +7,8 @@ module Storage
raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end end
expires_full_path_cache
# Move the namespace directory in all storage paths used by member projects # Move the namespace directory in all storage paths used by member projects
repository_storage_paths.each do |repository_storage_path| repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
......
...@@ -169,7 +169,7 @@ class Note < ActiveRecord::Base ...@@ -169,7 +169,7 @@ class Note < ActiveRecord::Base
end end
def cross_reference? def cross_reference?
system? && SystemNoteService.cross_reference?(note) system? && matches_cross_reference_regex?
end end
def diff_note? def diff_note?
......
...@@ -162,7 +162,6 @@ module SystemNoteService ...@@ -162,7 +162,6 @@ module SystemNoteService
# "changed time estimate to 3d 5h" # "changed time estimate to 3d 5h"
# #
# Returns the created Note object # Returns the created Note object
def change_time_estimate(noteable, project, author) def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0 body = if noteable.time_estimate == 0
...@@ -188,7 +187,6 @@ module SystemNoteService ...@@ -188,7 +187,6 @@ module SystemNoteService
# "added 2h 30m of time spent" # "added 2h 30m of time spent"
# #
# Returns the created Note object # Returns the created Note object
def change_time_spent(noteable, project, author) def change_time_spent(noteable, project, author)
time_spent = noteable.time_spent time_spent = noteable.time_spent
...@@ -453,10 +451,6 @@ module SystemNoteService ...@@ -453,10 +451,6 @@ module SystemNoteService
end end
end end
def cross_reference?(note_text)
note_text =~ /\A#{cross_reference_note_prefix}/i
end
# Check if a cross-reference is disallowed # Check if a cross-reference is disallowed
# #
# This method prevents adding a "mentioned in !1" note on every single commit # This method prevents adding a "mentioned in !1" note on every single commit
...@@ -486,7 +480,6 @@ module SystemNoteService ...@@ -486,7 +480,6 @@ module SystemNoteService
# mentioner - Mentionable object # mentioner - Mentionable object
# #
# Returns Boolean # Returns Boolean
def cross_reference_exists?(noteable, mentioner) def cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type # Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class) notes = Note.system.where(noteable_type: noteable.class)
......
---
title: Move project repositories between namespaces when renaming users
merge_request:
author:
type: security
---
title: Prevent an open redirect on project pages
merge_request:
author:
type: security
---
title: Prevent a persistent XSS in user-provided markup
merge_request:
author:
type: security
...@@ -75,9 +75,19 @@ module Banzai ...@@ -75,9 +75,19 @@ module Banzai
begin begin
node['href'] = node['href'].strip node['href'] = node['href'].strip
uri = Addressable::URI.parse(node['href']) uri = Addressable::URI.parse(node['href'])
uri.scheme = uri.scheme.downcase if uri.scheme
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(uri.scheme) return unless uri.scheme
# Remove all invalid scheme characters before checking against the
# list of unsafe protocols.
#
# See https://tools.ietf.org/html/rfc3986#section-3.1
scheme = uri.scheme
.strip
.downcase
.gsub(/[^A-Za-z0-9\+\.\-]+/, '')
node.remove_attribute('href') if UNSAFE_PROTOCOLS.include?(scheme)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
node.remove_attribute('href') node.remove_attribute('href')
end end
......
require('spec_helper') require('spec_helper')
describe ProfilesController do describe ProfilesController, :request_store do
describe "PUT update" do let(:user) { create(:user) }
it "allows an email update from a user without an external email address" do
user = create(:user) describe 'PUT update' do
it 'allows an email update from a user without an external email address' do
sign_in(user) sign_in(user)
put :update, put :update,
...@@ -29,7 +30,7 @@ describe ProfilesController do ...@@ -29,7 +30,7 @@ describe ProfilesController do
expect(user.unconfirmed_email).to eq nil expect(user.unconfirmed_email).to eq nil
end end
it "ignores an email update from a user with an external email address" do it 'ignores an email update from a user with an external email address' do
stub_omniauth_setting(sync_profile_from_provider: ['ldap']) stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true) stub_omniauth_setting(sync_profile_attributes: true)
...@@ -46,7 +47,7 @@ describe ProfilesController do ...@@ -46,7 +47,7 @@ describe ProfilesController do
expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
end end
it "ignores an email and name update but allows a location update from a user with external email and name, but not external location" do it 'ignores an email and name update but allows a location update from a user with external email and name, but not external location' do
stub_omniauth_setting(sync_profile_from_provider: ['ldap']) stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
stub_omniauth_setting(sync_profile_attributes: true) stub_omniauth_setting(sync_profile_attributes: true)
...@@ -65,4 +66,35 @@ describe ProfilesController do ...@@ -65,4 +66,35 @@ describe ProfilesController do
expect(ldap_user.location).to eq('City, Country') expect(ldap_user.location).to eq('City, Country')
end end
end end
describe 'PUT update_username' do
let(:namespace) { user.namespace }
let(:project) { create(:project_empty_repo, namespace: namespace) }
let(:gitlab_shell) { Gitlab::Shell.new }
let(:new_username) { 'renamedtosomethingelse' }
it 'allows username change' do
sign_in(user)
put :update_username,
user: { username: new_username }
user.reload
expect(response.status).to eq(302)
expect(user.username).to eq(new_username)
end
it 'moves dependent projects to new namespace' do
sign_in(user)
put :update_username,
user: { username: new_username }
user.reload
expect(response.status).to eq(302)
expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy
end
end
end end
...@@ -850,47 +850,48 @@ describe Projects::IssuesController do ...@@ -850,47 +850,48 @@ describe Projects::IssuesController do
describe 'GET #discussions' do describe 'GET #discussions' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) } let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
context 'when authenticated' do
before do
project.add_developer(user)
sign_in(user)
end
it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end
context 'with cross-reference system note', :request_store do
let(:new_issue) { create(:issue) }
let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
before do before do
create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference) project.add_developer(user)
sign_in(user)
end end
it 'filters notes that the user should not see' do it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(JSON.parse(response.body).count).to eq(1) expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end end
it 'does not result in N+1 queries' do context 'with cross-reference system note', :request_store do
# Instantiate the controller variables to ensure QueryRecorder has an accurate base count let(:new_issue) { create(:issue) }
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
RequestStore.clear! before do
create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
end
control_count = ActiveRecord::QueryRecorder.new do it 'filters notes that the user should not see' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
end.count
RequestStore.clear! expect(JSON.parse(response.body).count).to eq(1)
end
create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference) it 'does not result in N+1 queries' do
# Instantiate the controller variables to ensure QueryRecorder has an accurate base count
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count) RequestStore.clear!
control_count = ActiveRecord::QueryRecorder.new do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
end.count
RequestStore.clear!
create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
end
end end
end end
end end
......
...@@ -791,6 +791,29 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -791,6 +791,29 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
const avatar = tokenValueElement.querySelector('img.avatar'); const avatar = tokenValueElement.querySelector('img.avatar');
expect(avatar.src).toBe(dummyUser.avatar_url); expect(avatar.src).toBe(dummyUser.avatar_url);
expect(avatar.alt).toBe('');
})
.then(done)
.catch(done.fail);
});
it('escapes user name when creating token', (done) => {
const dummyUser = {
name: '<script>',
avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
};
const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
usersCacheSpy = (username) => {
expect(`@${username}`).toBe(tokenValue);
return Promise.resolve(dummyUser);
};
subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
.then(() => {
expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
tokenValueElement.querySelector('.avatar').remove();
expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name));
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
......
...@@ -217,6 +217,11 @@ describe Banzai::Filter::SanitizationFilter do ...@@ -217,6 +217,11 @@ describe Banzai::Filter::SanitizationFilter do
output: '<img>' output: '<img>'
}, },
'protocol-based JS injection: Unicode' => {
input: %Q(<a href="\u0001java\u0003script:alert('XSS')">foo</a>),
output: '<a>foo</a>'
},
'protocol-based JS injection: spaces and entities' => { 'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>', input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
output: '<a href="">foo</a>' output: '<a href="">foo</a>'
......
...@@ -4,6 +4,7 @@ describe Namespace do ...@@ -4,6 +4,7 @@ describe Namespace do
include ProjectForksHelper include ProjectForksHelper
let!(:namespace) { create(:namespace) } let!(:namespace) { create(:namespace) }
let(:gitlab_shell) { Gitlab::Shell.new }
describe 'associations' do describe 'associations' do
it { is_expected.to have_many :projects } it { is_expected.to have_many :projects }
...@@ -153,25 +154,18 @@ describe Namespace do ...@@ -153,25 +154,18 @@ describe Namespace do
end end
end end
describe '#move_dir' do describe '#move_dir', :request_store do
let(:namespace) { create(:namespace) } let(:namespace) { create(:namespace) }
let!(:project) { create(:project_empty_repo, namespace: namespace) } let!(:project) { create(:project_empty_repo, namespace: namespace) }
before do
allow(namespace).to receive(:path_changed?).and_return(true)
end
it "raises error when directory exists" do it "raises error when directory exists" do
expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved") expect { namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end end
it "moves dir if path changed" do it "moves dir if path changed" do
new_path = namespace.full_path + "_new" namespace.update_attributes(path: namespace.full_path + '_new')
allow(namespace).to receive(:full_path_was).and_return(namespace.full_path) expect(gitlab_shell.exists?(project.repository_storage_path, "#{namespace.path}/#{project.path}.git")).to be_truthy
allow(namespace).to receive(:full_path).and_return(new_path)
expect(namespace).to receive(:remove_exports!)
expect(namespace.move_dir).to be_truthy
end end
context "when any project has container images" do context "when any project has container images" do
......
...@@ -502,20 +502,6 @@ describe SystemNoteService do ...@@ -502,20 +502,6 @@ describe SystemNoteService do
end end
end end
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
expect(described_class.cross_reference?('mentioned in something')).to be_truthy
end
it 'is truthy when text begins with legacy capitalized expected text' do
expect(described_class.cross_reference?('mentioned in something')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
expect(described_class.cross_reference?('this is a note')).to be_falsey
end
end
describe '.cross_reference_disallowed?' do describe '.cross_reference_disallowed?' do
context 'when mentioner is not a MergeRequest' do context 'when mentioner is not a MergeRequest' do
it 'is falsey' do it 'is falsey' 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