Commit 7698d405 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' of dev.gitlab.org:gitlab/gitlabhq

parents fa160c26 09095625
...@@ -2,6 +2,38 @@ ...@@ -2,6 +2,38 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 12.2.3
### Security (22 changes)
- Ensure only authorised users can create notes on Merge Requests and Issues.
- Gitaly: ignore git redirects.
- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly.
- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds.
- Added image proxy to mitigate potential stealing of IP addresses.
- Filter out old system notes for epics in notes api endpoint response.
- Avoid exposing unaccessible repo data upon GFM post processing.
- Fix HTML injection for label description.
- Make sure HTML text is always escaped when replacing label/milestone references.
- Prevent DNS rebind on JIRA service integration.
- Use admin_group authorization in Groups::RunnersController.
- Prevent disclosure of merge request ID via email.
- Show cross-referenced MR-id in issues' activities only to authorized users.
- Enforce max chars and max render time in markdown math.
- Check permissions before responding in MergeController#pipeline_status.
- Remove EXIF from users/personal snippet uploads.
- Fix project import restricted visibility bypass via API.
- Fix weak session management by clearing password reset tokens after login (username/email) are updated.
- Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.2.2
- Unreleased due to QA failure.
## 12.2.1 ## 12.2.1
### Fixed (3 changes) ### Fixed (3 changes)
...@@ -591,6 +623,34 @@ entry. ...@@ -591,6 +623,34 @@ entry.
- Removes EE differences for app/views/admin/users/show.html.haml. - Removes EE differences for app/views/admin/users/show.html.haml.
## 12.0.7
### Security (22 changes)
- Ensure only authorised users can create notes on Merge Requests and Issues.
- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
- Queries for Upload should be scoped by model.
- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth.
- Limit the size of issuable description and comments.
- Send TODOs for comments on commits correctly.
- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds.
- Added image proxy to mitigate potential stealing of IP addresses.
- Filter out old system notes for epics in notes api endpoint response.
- Avoid exposing unaccessible repo data upon GFM post processing.
- Fix HTML injection for label description.
- Make sure HTML text is always escaped when replacing label/milestone references.
- Prevent DNS rebind on JIRA service integration.
- Use admin_group authorization in Groups::RunnersController.
- Prevent disclosure of merge request ID via email.
- Show cross-referenced MR-id in issues' activities only to authorized users.
- Enforce max chars and max render time in markdown math.
- Check permissions before responding in MergeController#pipeline_status.
- Remove EXIF from users/personal snippet uploads.
- Fix project import restricted visibility bypass via API.
- Fix weak session management by clearing password reset tokens after login (username/email) are updated.
- Fix SSRF via DNS rebinding in Kubernetes Integration.
## 12.0.6 ## 12.0.6
- No changes. - No changes.
......
import $ from 'jquery';
import { __ } from '~/locale';
import flash from '~/flash'; import flash from '~/flash';
import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the // Renders math using KaTeX in any element with the
// `js-render-math` class // `js-render-math` class
...@@ -10,21 +9,131 @@ import flash from '~/flash'; ...@@ -10,21 +9,131 @@ import flash from '~/flash';
// <code class="js-render-math"></div> // <code class="js-render-math"></div>
// //
// Loop over all math elements and render math const MAX_MATH_CHARS = 1000;
function renderWithKaTeX(elements, katex) { const MAX_RENDER_TIME_MS = 2000;
elements.each(function katexElementsLoop() {
const mathNode = $('<span></span>'); // These messages might be used with inline errors in the future. Keep them around. For now, we will
const $this = $(this); // display a single error message using flash().
const display = $this.attr('data-math-style') === 'display'; // const CHAR_LIMIT_EXCEEDED_MSG = sprintf(
try { // s__(
katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); // 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.',
mathNode.insertAfter($this); // ),
$this.remove(); // { maxChars: MAX_MATH_CHARS },
} catch (err) { // );
throw err; // const RENDER_TIME_EXCEEDED_MSG = s__(
// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.",
// );
const RENDER_FLASH_MSG = sprintf(
s__(
'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.',
),
{ maxChars: MAX_MATH_CHARS },
);
// Wait for the browser to reflow the layout. Reflowing SVG takes time.
// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object".
const waitForReflow = fn => {
window.requestAnimationFrame(fn);
};
/**
* Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown.
*/
class SafeMathRenderer {
/*
How this works:
The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG.
During this time, the JS is blocked and the page becomes unresponsive.
We want to render math blocks one by one until a certain time is exceeded, after which we stop
rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an
asynchronous task, so we can't time it synchronously.
SafeMathRenderer essentially does the following:
1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice.
2. Places each placeholder element in a queue.
3. Renders the element at the head of the queue and waits for reflow.
4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty.
*/
queue = [];
totalMS = 0;
constructor(elements, katex) {
this.elements = elements;
this.katex = katex;
this.renderElement = this.renderElement.bind(this);
this.render = this.render.bind(this);
}
renderElement() {
if (!this.queue.length) {
return;
} }
});
const el = this.queue.shift();
const text = el.textContent;
el.removeAttribute('style');
if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) {
if (!this.flashShown) {
flash(RENDER_FLASH_MSG);
this.flashShown = true;
}
// Show unrendered math code
const codeElement = document.createElement('pre');
codeElement.className = 'code';
codeElement.textContent = el.textContent;
el.parentNode.replaceChild(codeElement, el);
// Render the next math
this.renderElement();
} else {
this.startTime = Date.now();
try {
el.innerHTML = this.katex.renderToString(text, {
displayMode: el.getAttribute('data-math-style') === 'display',
throwOnError: true,
maxSize: 20,
maxExpand: 20,
});
} catch {
// Don't show a flash for now because it would override an existing flash message
el.textContent = s__('math|There was an error rendering this math block');
// el.style.color = '#d00';
el.className = 'katex-error';
}
// Give the browser time to reflow the svg
waitForReflow(() => {
const deltaTime = Date.now() - this.startTime;
this.totalMS += deltaTime;
this.renderElement();
});
}
}
render() {
// Replace math blocks with a placeholder so they aren't rendered twice
this.elements.forEach(el => {
const placeholder = document.createElement('span');
placeholder.style.display = 'none';
placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
placeholder.textContent = el.textContent;
el.parentNode.replaceChild(placeholder, el);
this.queue.push(placeholder);
});
// If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster
// and less prone to timeouts.
setTimeout(this.renderElement, 400);
}
} }
export default function renderMath($els) { export default function renderMath($els) {
...@@ -34,7 +143,8 @@ export default function renderMath($els) { ...@@ -34,7 +143,8 @@ export default function renderMath($els) {
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
]) ])
.then(([katex]) => { .then(([katex]) => {
renderWithKaTeX($els, katex); const renderer = new SafeMathRenderer($els.get(), katex);
renderer.render();
}) })
.catch(() => flash(__('An error occurred while rendering KaTeX'))); .catch(() => {});
} }
...@@ -138,7 +138,7 @@ module IssuableActions ...@@ -138,7 +138,7 @@ module IssuableActions
end end
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = notes.select { |n| n.visible_for?(current_user) }
discussions = Discussion.build_collection(notes, issuable) discussions = Discussion.build_collection(notes, issuable)
......
...@@ -29,7 +29,7 @@ module NotesActions ...@@ -29,7 +29,7 @@ module NotesActions
end end
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = notes.select { |n| n.visible_for?(current_user) }
notes_json[:notes] = notes_json[:notes] =
if use_note_serializer? if use_note_serializer?
......
...@@ -127,4 +127,8 @@ module UploadsActions ...@@ -127,4 +127,8 @@ module UploadsActions
def model def model
strong_memoize(:model) { find_model } strong_memoize(:model) { find_model }
end end
def workhorse_authorize_request?
action_name == 'authorize'
end
end end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
class Groups::RunnersController < Groups::ApplicationController class Groups::RunnersController < Groups::ApplicationController
# Proper policies should be implemented per # Proper policies should be implemented per
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
before_action :authorize_admin_pipeline! before_action :authorize_admin_group!
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
...@@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController ...@@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController
@runner ||= @group.runners.find(params[:id]) @runner ||= @group.runners.find(params[:id])
end end
def authorize_admin_pipeline!
return render_404 unless can?(current_user, :admin_pipeline, group)
end
def runner_params def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end end
......
...@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update] before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authorize_test_reports!, only: [:test_reports]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action :check_user_can_push_to_source_branch!, only: [:rebase]
...@@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -189,7 +190,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def pipeline_status def pipeline_status
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
.represent_status(@merge_request.head_pipeline) .represent_status(head_pipeline)
end end
def ci_environments_status def ci_environments_status
...@@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -239,6 +240,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private private
def head_pipeline
strong_memoize(:head_pipeline) do
pipeline = @merge_request.head_pipeline
pipeline if can?(current_user, :read_pipeline, pipeline)
end
end
def ci_environments_status_on_merge_result? def ci_environments_status_on_merge_result?
params[:environment_target] == 'merge_commit' params[:environment_target] == 'merge_commit'
end end
...@@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -337,4 +345,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
render json: { status_reason: 'Unknown error' }, status: :internal_server_error render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end end
end end
def authorize_test_reports!
# MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports.
return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline)
end
end end
...@@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController ...@@ -21,10 +21,13 @@ class SessionsController < Devise::SessionsController
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? } prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
before_action :auto_sign_in_with_provider, only: [:new] before_action :auto_sign_in_with_provider, only: [:new]
before_action :store_unauthenticated_sessions, only: [:new]
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha before_action :load_recaptcha
after_action :log_failed_login, if: -> { action_name == 'new' && failed_login? } after_action :log_failed_login, if: :action_new_and_failed_login?
helper_method :captcha_enabled?
helper_method :captcha_enabled?, :captcha_on_login_required?
# protect_from_forgery is already prepended in ApplicationController but # protect_from_forgery is already prepended in ApplicationController but
# authenticate_with_two_factor which signs in the user is prepended before # authenticate_with_two_factor which signs in the user is prepended before
...@@ -38,6 +41,7 @@ class SessionsController < Devise::SessionsController ...@@ -38,6 +41,7 @@ class SessionsController < Devise::SessionsController
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
MAX_FAILED_LOGIN_ATTEMPTS = 5
def new def new
set_minimum_password_length set_minimum_password_length
...@@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController ...@@ -81,10 +85,14 @@ class SessionsController < Devise::SessionsController
request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled? request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
end end
def captcha_on_login_required?
Gitlab::Recaptcha.enabled_on_login? && unverified_anonymous_user?
end
# From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller # From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
def check_captcha def check_captcha
return unless user_params[:password].present? return unless user_params[:password].present?
return unless captcha_enabled? return unless captcha_enabled? || captcha_on_login_required?
return unless Gitlab::Recaptcha.load_configurations! return unless Gitlab::Recaptcha.load_configurations!
if verify_recaptcha if verify_recaptcha
...@@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController ...@@ -126,10 +134,28 @@ class SessionsController < Devise::SessionsController
Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}") Gitlab::AppLogger.info("Failed Login: username=#{user_params[:login]} ip=#{request.remote_ip}")
end end
def action_new_and_failed_login?
action_name == 'new' && failed_login?
end
def save_failed_login
session[:failed_login_attempts] ||= 0
session[:failed_login_attempts] += 1
end
def failed_login? def failed_login?
(options = request.env["warden.options"]) && options[:action] == "unauthenticated" (options = request.env["warden.options"]) && options[:action] == "unauthenticated"
end end
# storing sessions per IP lets us check if there are associated multiple
# anonymous sessions with one IP and prevent situations when there are
# multiple attempts of logging in
def store_unauthenticated_sessions
return if current_user
Gitlab::AnonymousSession.new(request.remote_ip, session_id: request.session.id).store_session_id_per_ip
end
# Handle an "initial setup" state, where there's only one user, it's an admin, # Handle an "initial setup" state, where there's only one user, it's an admin,
# and they require a password change. # and they require a password change.
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController ...@@ -240,6 +266,18 @@ class SessionsController < Devise::SessionsController
@ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_servers
end end
def unverified_anonymous_user?
exceeded_failed_login_attempts? || exceeded_anonymous_sessions?
end
def exceeded_failed_login_attempts?
session.fetch(:failed_login_attempts, 0) > MAX_FAILED_LOGIN_ATTEMPTS
end
def exceeded_anonymous_sessions?
Gitlab::AnonymousSession.new(request.remote_ip).stored_sessions >= MAX_FAILED_LOGIN_ATTEMPTS
end
def authentication_method def authentication_method
if user_params[:otp_attempt] if user_params[:otp_attempt]
"two-factor" "two-factor"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class UploadsController < ApplicationController class UploadsController < ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest
UnknownUploadModelError = Class.new(StandardError) UnknownUploadModelError = Class.new(StandardError)
...@@ -21,7 +22,8 @@ class UploadsController < ApplicationController ...@@ -21,7 +22,8 @@ class UploadsController < ApplicationController
before_action :upload_mount_satisfied? before_action :upload_mount_satisfied?
before_action :find_model before_action :find_model
before_action :authorize_access!, only: [:show] before_action :authorize_access!, only: [:show]
before_action :authorize_create_access!, only: [:create] before_action :authorize_create_access!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
def uploader_class def uploader_class
PersonalFileUploader PersonalFileUploader
...@@ -72,7 +74,7 @@ class UploadsController < ApplicationController ...@@ -72,7 +74,7 @@ class UploadsController < ApplicationController
end end
def render_unauthorized def render_unauthorized
if current_user if current_user || workhorse_authorize_request?
render_404 render_404
else else
authenticate_user! authenticate_user!
......
...@@ -164,6 +164,10 @@ module ApplicationSettingsHelper ...@@ -164,6 +164,10 @@ module ApplicationSettingsHelper
:allow_local_requests_from_system_hooks, :allow_local_requests_from_system_hooks,
:dns_rebinding_protection_enabled, :dns_rebinding_protection_enabled,
:archive_builds_in_human_readable, :archive_builds_in_human_readable,
:asset_proxy_enabled,
:asset_proxy_secret_key,
:asset_proxy_url,
:asset_proxy_whitelist,
:authorized_keys_enabled, :authorized_keys_enabled,
:auto_devops_enabled, :auto_devops_enabled,
:auto_devops_domain, :auto_devops_domain,
...@@ -231,6 +235,7 @@ module ApplicationSettingsHelper ...@@ -231,6 +235,7 @@ module ApplicationSettingsHelper
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_private_key, :recaptcha_private_key,
:recaptcha_site_key, :recaptcha_site_key,
:login_recaptcha_protection_enabled,
:receive_max_input_size, :receive_max_input_size,
:repository_checks_enabled, :repository_checks_enabled,
:repository_storages, :repository_storages,
......
...@@ -90,6 +90,8 @@ module EmailsHelper ...@@ -90,6 +90,8 @@ module EmailsHelper
when MergeRequest when MergeRequest
merge_request = MergeRequest.find(closed_via[:id]).present merge_request = MergeRequest.find(closed_via[:id]).present
return "" unless Ability.allowed?(@recipient, :read_merge_request, merge_request)
case format case format
when :html when :html
merge_request_link = link_to(merge_request.to_reference, merge_request.web_url) merge_request_link = link_to(merge_request.to_reference, merge_request.web_url)
...@@ -102,6 +104,8 @@ module EmailsHelper ...@@ -102,6 +104,8 @@ module EmailsHelper
# Technically speaking this should be Commit but per # Technically speaking this should be Commit but per
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
# we can't deserialize Commit without custom serializer for ActiveJob # we can't deserialize Commit without custom serializer for ActiveJob
return "" unless Ability.allowed?(@recipient, :download_code, @project)
_("via %{closed_via}") % { closed_via: closed_via } _("via %{closed_via}") % { closed_via: closed_via }
else else
"" ""
......
...@@ -71,7 +71,7 @@ module LabelsHelper ...@@ -71,7 +71,7 @@ module LabelsHelper
end end
def label_tooltip_title(label) def label_tooltip_title(label)
label.description Sanitize.clean(label.description)
end end
def suggested_colors def suggested_colors
......
...@@ -34,6 +34,8 @@ module Emails ...@@ -34,6 +34,8 @@ module Emails
setup_issue_mail(issue_id, recipient_id, closed_via: closed_via) setup_issue_mail(issue_id, recipient_id, closed_via: closed_via)
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
@recipient = User.find(recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
......
...@@ -18,12 +18,19 @@ class ApplicationSetting < ApplicationRecord ...@@ -18,12 +18,19 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of # fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation include ApplicationSettingImplementation
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
insecure_mode: true,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
serialize :asset_proxy_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize
ignore_column :koding_url ignore_column :koding_url
ignore_column :koding_enabled ignore_column :koding_enabled
...@@ -75,11 +82,11 @@ class ApplicationSetting < ApplicationRecord ...@@ -75,11 +82,11 @@ class ApplicationSetting < ApplicationRecord
validates :recaptcha_site_key, validates :recaptcha_site_key,
presence: true, presence: true,
if: :recaptcha_enabled if: :recaptcha_or_login_protection_enabled
validates :recaptcha_private_key, validates :recaptcha_private_key,
presence: true, presence: true,
if: :recaptcha_enabled if: :recaptcha_or_login_protection_enabled
validates :akismet_api_key, validates :akismet_api_key,
presence: true, presence: true,
...@@ -192,6 +199,17 @@ class ApplicationSetting < ApplicationRecord ...@@ -192,6 +199,17 @@ class ApplicationSetting < ApplicationRecord
allow_nil: true, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 } numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 65536 }
validates :asset_proxy_url,
presence: true,
allow_blank: false,
url: true,
if: :asset_proxy_enabled?
validates :asset_proxy_secret_key,
presence: true,
allow_blank: false,
if: :asset_proxy_enabled?
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -292,4 +310,8 @@ class ApplicationSetting < ApplicationRecord ...@@ -292,4 +310,8 @@ class ApplicationSetting < ApplicationRecord
def self.cache_backend def self.cache_backend
Gitlab::ThreadMemoryCache.cache_backend Gitlab::ThreadMemoryCache.cache_backend
end end
def recaptcha_or_login_protection_enabled
recaptcha_enabled || login_recaptcha_protection_enabled
end
end end
...@@ -23,8 +23,9 @@ module ApplicationSettingImplementation ...@@ -23,8 +23,9 @@ module ApplicationSettingImplementation
akismet_enabled: false, akismet_enabled: false,
allow_local_requests_from_web_hooks_and_services: false, allow_local_requests_from_web_hooks_and_services: false,
allow_local_requests_from_system_hooks: true, allow_local_requests_from_system_hooks: true,
dns_rebinding_protection_enabled: true, asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days', default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'], default_branch_protection: Settings.gitlab['default_branch_protection'],
...@@ -33,7 +34,9 @@ module ApplicationSettingImplementation ...@@ -33,7 +34,9 @@ module ApplicationSettingImplementation
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,
domain_whitelist: Settings.gitlab['domain_whitelist'], domain_whitelist: Settings.gitlab['domain_whitelist'],
dsa_key_restriction: 0, dsa_key_restriction: 0,
ecdsa_key_restriction: 0, ecdsa_key_restriction: 0,
...@@ -52,9 +55,11 @@ module ApplicationSettingImplementation ...@@ -52,9 +55,11 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'], import_sources: Settings.gitlab['import_sources'],
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'], max_attachment_size: Settings.gitlab['max_attachment_size'],
mirror_available: true, mirror_available: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true, password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil, performance_bar_allowed_group_id: nil,
...@@ -63,7 +68,10 @@ module ApplicationSettingImplementation ...@@ -63,7 +68,10 @@ module ApplicationSettingImplementation
plantuml_url: nil, plantuml_url: nil,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
project_export_enabled: true, project_export_enabled: true,
protected_ci_variables: false,
raw_blob_request_limit: 300,
recaptcha_enabled: false, recaptcha_enabled: false,
login_recaptcha_protection_enabled: false,
repository_checks_enabled: true, repository_checks_enabled: true,
repository_storages: ['default'], repository_storages: ['default'],
require_two_factor_authentication: false, require_two_factor_authentication: false,
...@@ -95,16 +103,10 @@ module ApplicationSettingImplementation ...@@ -95,16 +103,10 @@ module ApplicationSettingImplementation
user_default_internal_regex: nil, user_default_internal_regex: nil,
user_show_add_ssh_key_message: true, user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil, usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname,
snowplow_collector_hostname: nil, snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil, snowplow_cookie_domain: nil,
snowplow_enabled: false, snowplow_enabled: false,
snowplow_site_id: nil, snowplow_site_id: nil
protected_ci_variables: false,
local_markdown_version: 0,
outbound_local_requests_whitelist: [],
raw_blob_request_limit: 300
} }
end end
...@@ -198,6 +200,15 @@ module ApplicationSettingImplementation ...@@ -198,6 +200,15 @@ module ApplicationSettingImplementation
end end
end end
def asset_proxy_whitelist=(values)
values = domain_strings_to_array(values) if values.is_a?(String)
# make sure we always whitelist the running host
values << Gitlab.config.gitlab.host unless values.include?(Gitlab.config.gitlab.host)
self[:asset_proxy_whitelist] = values
end
def repository_storages def repository_storages
Array(read_attribute(:repository_storages)) Array(read_attribute(:repository_storages))
end end
...@@ -306,6 +317,7 @@ module ApplicationSettingImplementation ...@@ -306,6 +317,7 @@ module ApplicationSettingImplementation
values values
.split(DOMAIN_LIST_SEPARATOR) .split(DOMAIN_LIST_SEPARATOR)
.map(&:strip)
.reject(&:empty?) .reject(&:empty?)
.uniq .uniq
end end
......
...@@ -203,6 +203,7 @@ module Ci ...@@ -203,6 +203,7 @@ module Ci
scope :for_sha, -> (sha) { where(sha: sha) } scope :for_sha, -> (sha) { where(sha: sha) }
scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
scope :triggered_by_merge_request, -> (merge_request) do scope :triggered_by_merge_request, -> (merge_request) do
where(source: :merge_request_event, merge_request: merge_request) where(source: :merge_request_event, merge_request: merge_request)
......
...@@ -73,6 +73,7 @@ module Issuable ...@@ -73,6 +73,7 @@ module Issuable
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true
validate :milestone_is_valid validate :milestone_is_valid
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
......
...@@ -365,6 +365,8 @@ class Group < Namespace ...@@ -365,6 +365,8 @@ class Group < Namespace
end end
def max_member_access_for_user(user) def max_member_access_for_user(user)
return GroupMember::NO_ACCESS unless user
return GroupMember::OWNER if user.admin? return GroupMember::OWNER if user.admin?
members_with_parents members_with_parents
......
...@@ -199,7 +199,11 @@ class Label < ApplicationRecord ...@@ -199,7 +199,11 @@ class Label < ApplicationRecord
end end
def title=(value) def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present? write_attribute(:title, sanitize_value(value)) if value.present?
end
def description=(value)
write_attribute(:description, sanitize_value(value)) if value.present?
end end
## ##
...@@ -260,7 +264,7 @@ class Label < ApplicationRecord ...@@ -260,7 +264,7 @@ class Label < ApplicationRecord
end end
end end
def sanitize_title(value) def sanitize_value(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s)) CGI.unescapeHTML(Sanitize.clean(value.to_s))
end end
......
...@@ -89,6 +89,7 @@ class Note < ApplicationRecord ...@@ -89,6 +89,7 @@ class Note < ApplicationRecord
delegate :title, to: :noteable, allow_nil: true delegate :title, to: :noteable, allow_nil: true
validates :note, presence: true validates :note, presence: true
validates :note, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }
validates :project, presence: true, if: :for_project_noteable? validates :project, presence: true, if: :for_project_noteable?
# Attachments are deprecated and are handled by Markdown uploader # Attachments are deprecated and are handled by Markdown uploader
...@@ -331,6 +332,10 @@ class Note < ApplicationRecord ...@@ -331,6 +332,10 @@ class Note < ApplicationRecord
cross_reference? && !all_referenced_mentionables_allowed?(user) cross_reference? && !all_referenced_mentionables_allowed?(user)
end end
def visible_for?(user)
!cross_reference_not_visible_for?(user)
end
def award_emoji? def award_emoji?
can_be_award_emoji? && contains_emoji_only? can_be_award_emoji? && contains_emoji_only?
end end
......
...@@ -64,7 +64,12 @@ class JiraService < IssueTrackerService ...@@ -64,7 +64,12 @@ class JiraService < IssueTrackerService
end end
def client def client
@client ||= JIRA::Client.new(options) @client ||= begin
JIRA::Client.new(options).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
end end
def help def help
......
...@@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord ...@@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord
TYPES_WITH_CROSS_REFERENCES = %w[ TYPES_WITH_CROSS_REFERENCES = %w[
commit cross_reference commit cross_reference
close duplicate close duplicate
moved moved merge
].freeze ].freeze
ICON_TYPES = %w[ ICON_TYPES = %w[
......
...@@ -645,6 +645,13 @@ class User < ApplicationRecord ...@@ -645,6 +645,13 @@ class User < ApplicationRecord
end end
end end
# will_save_change_to_attribute? is used by Devise to check if it is necessary
# to clear any existing reset_password_tokens before updating an authentication_key
# and login in our case is a virtual attribute to allow login by username or email.
def will_save_change_to_login?
will_save_change_to_username? || will_save_change_to_email?
end
def unique_email def unique_email
if !emails.exists?(email: email) && Email.exists?(email: email) if !emails.exists?(email: email) && Email.exists?(email: email)
errors.add(:email, _('has already been taken')) errors.add(:email, _('has already been taken'))
......
...@@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy ...@@ -5,6 +5,8 @@ class IssuePolicy < IssuablePolicy
# Make sure to sync this class checks with issue.rb to avoid security problems. # Make sure to sync this class checks with issue.rb to avoid security problems.
# Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information. # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
extend ProjectPolicy::ClassMethods
desc "User can read confidential issues" desc "User can read confidential issues"
condition(:can_read_confidential) do condition(:can_read_confidential) do
@user && IssueCollection.new([@subject]).visible_to(@user).any? @user && IssueCollection.new([@subject]).visible_to(@user).any?
...@@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy ...@@ -14,13 +16,12 @@ class IssuePolicy < IssuablePolicy
condition(:confidential, scope: :subject) { @subject.confidential? } condition(:confidential, scope: :subject) { @subject.confidential? }
rule { confidential & ~can_read_confidential }.policy do rule { confidential & ~can_read_confidential }.policy do
prevent :read_issue prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid prevent :read_issue_iid
prevent :update_issue
prevent :admin_issue
prevent :create_note
end end
rule { ~can?(:read_issue) }.prevent :create_note
rule { locked }.policy do rule { locked }.policy do
prevent :reopen_issue prevent :reopen_issue
end end
......
...@@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy ...@@ -4,4 +4,10 @@ class MergeRequestPolicy < IssuablePolicy
rule { locked }.policy do rule { locked }.policy do
prevent :reopen_merge_request prevent :reopen_merge_request
end end
# Only users who can read the merge request can comment.
# Although :read_merge_request is computed in the policy context,
# it would not be safe to prevent :create_note there, since
# note permissions are shared, and this would apply too broadly.
rule { ~can?(:read_merge_request) }.prevent :create_note
end end
...@@ -6,6 +6,8 @@ module ApplicationSettings ...@@ -6,6 +6,8 @@ module ApplicationSettings
attr_reader :params, :application_setting attr_reader :params, :application_setting
MARKDOWN_CACHE_INVALIDATING_PARAMS = %w(asset_proxy_enabled asset_proxy_url asset_proxy_secret_key asset_proxy_whitelist).freeze
def execute def execute
validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth? validate_classification_label(application_setting, :external_authorization_service_default_label) unless bypass_external_auth?
...@@ -25,7 +27,13 @@ module ApplicationSettings ...@@ -25,7 +27,13 @@ module ApplicationSettings
params[:usage_stats_set_by_user_id] = current_user.id params[:usage_stats_set_by_user_id] = current_user.id
end end
@application_setting.update(@params) @application_setting.assign_attributes(params)
if invalidate_markdown_cache?
@application_setting[:local_markdown_version] = @application_setting.local_markdown_version + 1
end
@application_setting.save
end end
private private
...@@ -41,6 +49,11 @@ module ApplicationSettings ...@@ -41,6 +49,11 @@ module ApplicationSettings
@application_setting.add_to_outbound_local_requests_whitelist(values_array) @application_setting.add_to_outbound_local_requests_whitelist(values_array)
end end
def invalidate_markdown_cache?
!params.key?(:local_markdown_version) &&
(@application_setting.changes.keys & MARKDOWN_CACHE_INVALIDATING_PARAMS).any?
end
def update_terms(terms) def update_terms(terms)
return unless terms.present? return unless terms.present?
......
...@@ -15,7 +15,8 @@ module Ci ...@@ -15,7 +15,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create, Gitlab::Ci::Pipeline::Chain::Create,
Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block)
@pipeline = Ci::Pipeline.new @pipeline = Ci::Pipeline.new
......
...@@ -5,9 +5,11 @@ module Projects ...@@ -5,9 +5,11 @@ module Projects
include ValidatesClassificationLabel include ValidatesClassificationLabel
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki) @skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme)) @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
@import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block)
end end
def execute def execute
...@@ -15,14 +17,11 @@ module Projects ...@@ -15,14 +17,11 @@ module Projects
return ::Projects::CreateFromTemplateService.new(current_user, params).execute return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end end
import_data = params.delete(:import_data)
relations_block = params.delete(:relations_block)
@project = Project.new(params) @project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level # Make sure that the user is allowed to use the specified visibility level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) if project_visibility.restricted?
deny_visibility_level(@project) deny_visibility_level(@project, project_visibility.visibility_level)
return @project return @project
end end
...@@ -44,7 +43,7 @@ module Projects ...@@ -44,7 +43,7 @@ module Projects
@project.namespace_id = current_user.namespace_id @project.namespace_id = current_user.namespace_id
end end
relations_block&.call(@project) @relations_block&.call(@project)
yield(@project) if block_given? yield(@project) if block_given?
validate_classification_label(@project, :external_authorization_classification_label) validate_classification_label(@project, :external_authorization_classification_label)
...@@ -54,7 +53,7 @@ module Projects ...@@ -54,7 +53,7 @@ module Projects
@project.creator = current_user @project.creator = current_user
save_project_and_import_data(import_data) save_project_and_import_data
after_create_actions if @project.persisted? after_create_actions if @project.persisted?
...@@ -129,9 +128,9 @@ module Projects ...@@ -129,9 +128,9 @@ module Projects
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
def save_project_and_import_data(import_data) def save_project_and_import_data
Project.transaction do Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data
if @project.save if @project.save
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
...@@ -192,5 +191,11 @@ module Projects ...@@ -192,5 +191,11 @@ module Projects
fail(error: @project.errors.full_messages.join(', ')) fail(error: @project.errors.full_messages.join(', '))
end end
end end
def project_visibility
@project_visibility ||= Gitlab::VisibilityLevelChecker
.new(current_user, @project, project_params: { import_data: @import_data })
.level_restricted?
end
end end
end end
...@@ -314,11 +314,9 @@ class TodoService ...@@ -314,11 +314,9 @@ class TodoService
end end
def reject_users_without_access(users, parent, target) def reject_users_without_access(users, parent, target)
if target.is_a?(Note) && target.for_issuable? target = target.noteable if target.is_a?(Note)
target = target.noteable
end
if target.is_a?(Issuable) if target.respond_to?(:to_ability_name)
select_users(users, :"read_#{target.to_ability_name}", target) select_users(users, :"read_#{target.to_ability_name}", target)
else else
select_users(users, :read_project, parent) select_users(users, :read_project, parent)
......
...@@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader ...@@ -6,6 +6,10 @@ class PersonalFileUploader < FileUploader
options.storage_path options.storage_path
end end
def self.workhorse_local_upload_path
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
end
def self.base_dir(model, _store = nil) def self.base_dir(model, _store = nil)
# base_dir is the path seen by the user when rendering Markdown, so # base_dir is the path seen by the user when rendering Markdown, so
# it should be the same for both local and object storage. It is # it should be the same for both local and object storage. It is
......
...@@ -7,11 +7,15 @@ ...@@ -7,11 +7,15 @@
= f.check_box :recaptcha_enabled, class: 'form-check-input' = f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do = f.label :recaptcha_enabled, class: 'form-check-label' do
Enable reCAPTCHA Enable reCAPTCHA
- recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
- recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
%span.form-text.text-muted#recaptcha_help_block %span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe } = _('Helps prevent bots from creating accounts.')
.form-group
.form-check
= f.check_box :login_recaptcha_protection_enabled, class: 'form-check-input'
= f.label :login_recaptcha_protection_enabled, class: 'form-check-label' do
Enable reCAPTCHA for login
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from brute-force attacks.')
.form-group .form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold' = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
= f.text_field :recaptcha_site_key, class: 'form-control' = f.text_field :recaptcha_site_key, class: 'form-control'
...@@ -21,6 +25,7 @@ ...@@ -21,6 +25,7 @@
.form-group .form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold' = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-bold'
.form-group
= f.text_field :recaptcha_private_key, class: 'form-control' = f.text_field :recaptcha_private_key, class: 'form-control'
.form-group .form-group
......
...@@ -9,7 +9,9 @@ ...@@ -9,7 +9,9 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' } %button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand') = expanded_by_default? ? _('Collapse') : _('Expand')
%p %p
= _('Enable reCAPTCHA or Akismet and set IP limits.') - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
- recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
= _('Enable reCAPTCHA or Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe }
.settings-content .settings-content
= render 'spam' = render 'spam'
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
- else - else
= link_to _('Forgot your password?'), new_password_path(:user) = link_to _('Forgot your password?'), new_password_path(:user)
%div %div
- if captcha_enabled? - if captcha_enabled? || captcha_on_login_required?
= recaptcha_tags = recaptcha_tags
.submit-container.move-submit-down .submit-container.move-submit-down
......
---
title: Ensure only authorised users can create notes on Merge Requests and Issues
type: security
---
title: Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks.
merge_request:
author:
type: security
---
title: Speed up regexp in namespace format by failing fast after reaching maximum namespace depth
merge_request:
author:
type: security
---
title: Limit the size of issuable description and comments
merge_request:
author:
type: security
---
title: Send TODOs for comments on commits correctly
merge_request:
author:
type: security
---
title: Restrict MergeRequests#test_reports to authenticated users with read-access
on Builds
merge_request:
author:
type: security
---
title: Added image proxy to mitigate potential stealing of IP addresses
merge_request:
author:
type: security
---
title: Filter out old system notes for epics in notes api endpoint response
merge_request:
author:
type: security
---
title: Avoid exposing unaccessible repo data upon GFM post processing
merge_request:
author:
type: security
---
title: Fix HTML injection for label description
merge_request:
author:
type: security
---
title: Make sure HTML text is always escaped when replacing label/milestone references.
merge_request:
author:
type: security
---
title: Prevent DNS rebind on JIRA service integration
merge_request:
author:
type: security
---
title: "Gitaly: ignore git redirects"
merge_request:
author:
type: security
---
title: Use admin_group authorization in Groups::RunnersController
merge_request:
author:
type: security
---
title: Prevent disclosure of merge request ID via email
merge_request:
author:
type: security
---
title: Show cross-referenced MR-id in issues' activities only to authorized users
merge_request:
author:
type: security
---
title: Enforce max chars and max render time in markdown math
merge_request:
author:
type: security
---
title: Check permissions before responding in MergeController#pipeline_status
merge_request:
author:
type: security
---
title: Remove EXIF from users/personal snippet uploads.
merge_request:
author:
type: security
---
title: Fix project import restricted visibility bypass via API
merge_request:
author:
type: security
---
title: Fix weak session management by clearing password reset tokens after login (username/email)
are updated
merge_request:
author:
type: security
---
title: Fix SSRF via DNS rebinding in Kubernetes Integration
merge_request:
author:
type: security
#
# Asset proxy settings
#
ActiveSupport.on_load(:active_record) do
Banzai::Filter::AssetProxyFilter.initialize_settings
end
if Shard.connected? && !Gitlab::Database.read_only? # The `table_exists?` check is needed because during our migration rollback testing,
# `Shard.connected?` could be cached and return true even though the table doesn't exist
if Shard.connected? && Shard.table_exists? && !Gitlab::Database.read_only?
Shard.populate! Shard.populate!
end end
# frozen_string_literal: true
module RestClient
class Request
attr_accessor :hostname_override
module UrlBlocker
def transmit(uri, req, payload, &block)
begin
ip, hostname_override = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_settings_local_requests?,
allow_localhost: allow_settings_local_requests?,
dns_rebind_protection: dns_rebind_protection?)
self.hostname_override = hostname_override
rescue Gitlab::UrlBlocker::BlockedUrlError => e
raise ArgumentError, "URL '#{uri}' is blocked: #{e.message}"
end
# Gitlab::UrlBlocker returns a Addressable::URI which we need to coerce
# to URI so that rest-client can use it to determine if it's a
# URI::HTTPS or not. It uses it to set `net.use_ssl` to true or not:
#
# https://github.com/rest-client/rest-client/blob/f450a0f086f1cd1049abbef2a2c66166a1a9ba71/lib/restclient/request.rb#L656
ip_as_uri = URI.parse(ip)
super(ip_as_uri, req, payload, &block)
end
def net_http_object(hostname, port)
super.tap do |http|
http.hostname_override = hostname_override if hostname_override
end
end
private
def dns_rebind_protection?
return false if Gitlab.http_proxy_env?
Gitlab::CurrentSettings.dns_rebinding_protection_enabled?
end
def allow_settings_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
end
prepend UrlBlocker
end
end
...@@ -19,6 +19,7 @@ Rails.application.configure do |config| ...@@ -19,6 +19,7 @@ Rails.application.configure do |config|
Warden::Manager.after_authentication(scope: :user) do |user, auth, opts| Warden::Manager.after_authentication(scope: :user) do |user, auth, opts|
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::AnonymousSession.new(auth.request.remote_ip, session_id: auth.request.session.id).cleanup_session_per_ip_entries
end end
Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts| Warden::Manager.after_set_user(scope: :user, only: :fetch) do |user, auth, opts|
......
...@@ -30,6 +30,10 @@ scope path: :uploads do ...@@ -30,6 +30,10 @@ scope path: :uploads do
to: 'uploads#create', to: 'uploads#create',
constraints: { model: /personal_snippet|user/, id: /\d+/ }, constraints: { model: /personal_snippet|user/, id: /\d+/ },
as: 'upload' as: 'upload'
post ':model/authorize',
to: 'uploads#authorize',
constraints: { model: /personal_snippet|user/ }
end end
# Redirect old note attachments path to new uploads path. # Redirect old note attachments path to new uploads path.
......
# frozen_string_literal: true
class AddAssetProxySettings < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
add_column :application_settings, :asset_proxy_enabled, :boolean, default: false, null: false
add_column :application_settings, :asset_proxy_url, :string # rubocop:disable Migration/AddLimitToStringColumns
add_column :application_settings, :asset_proxy_whitelist, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string # rubocop:disable Migration/AddLimitToStringColumns
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLoginRecaptchaProtectionEnabledToApplicationSettings < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
add_column :application_settings, :login_recaptcha_protection_enabled, :boolean, default: false, null: false
end
end
# frozen_string_literal: true
class AddActiveJobsLimitToPlans < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :plans, :active_jobs_limit, :integer, default: 0
end
def down
remove_column :plans, :active_jobs_limit
end
end
...@@ -272,12 +272,18 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do ...@@ -272,12 +272,18 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do
t.boolean "lock_memberships_to_ldap", default: false, null: false t.boolean "lock_memberships_to_ldap", default: false, null: false
t.boolean "time_tracking_limit_to_hours", default: false, null: false t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.string "grafana_url", default: "/-/grafana", null: false t.string "grafana_url", default: "/-/grafana", null: false
t.boolean "login_recaptcha_protection_enabled", default: false, null: false
t.string "outbound_local_requests_whitelist", limit: 255, default: [], null: false, array: true t.string "outbound_local_requests_whitelist", limit: 255, default: [], null: false, array: true
t.integer "raw_blob_request_limit", default: 300, null: false t.integer "raw_blob_request_limit", default: 300, null: false
t.boolean "allow_local_requests_from_web_hooks_and_services", default: false, null: false t.boolean "allow_local_requests_from_web_hooks_and_services", default: false, null: false
t.boolean "allow_local_requests_from_system_hooks", default: true, null: false t.boolean "allow_local_requests_from_system_hooks", default: true, null: false
t.bigint "instance_administration_project_id" t.bigint "instance_administration_project_id"
t.string "snowplow_collector_hostname" t.string "snowplow_collector_hostname"
t.boolean "asset_proxy_enabled", default: false, null: false
t.string "asset_proxy_url"
t.text "asset_proxy_whitelist"
t.text "encrypted_asset_proxy_secret_key"
t.string "encrypted_asset_proxy_secret_key_iv"
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id" t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id" t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id" t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
...@@ -2514,6 +2520,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do ...@@ -2514,6 +2520,7 @@ ActiveRecord::Schema.define(version: 2019_08_28_083843) do
t.string "title" t.string "title"
t.integer "active_pipelines_limit" t.integer "active_pipelines_limit"
t.integer "pipeline_size_limit" t.integer "pipeline_size_limit"
t.integer "active_jobs_limit", default: 0
t.index ["name"], name: "index_plans_on_name" t.index ["name"], name: "index_plans_on_name"
end end
......
...@@ -37,6 +37,8 @@ Parameter | Type | Description ...@@ -37,6 +37,8 @@ Parameter | Type | Description
`stop_id` | integer | Only uploads with equal or smaller ID will be processed `stop_id` | integer | Only uploads with equal or smaller ID will be processed
`dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not, default: true `dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not, default: true
`sleep_time` | float | Pause for number of seconds after processing each image, default: 0.3 seconds `sleep_time` | float | Pause for number of seconds after processing each image, default: 0.3 seconds
`uploader` | string | Run sanitization only for uploads of the given uploader (`FileUploader`, `PersonalFileUploader`, `NamespaceFileUploader`)
`since` | date | Run sanitization only for uploads newer than given date (e.g. `2019-05-01`)
If you have too many uploads, you can speed up sanitization by setting If you have too many uploads, you can speed up sanitization by setting
`sleep_time` to a lower value or by running multiple rake tasks in parallel, `sleep_time` to a lower value or by running multiple rake tasks in parallel,
......
...@@ -165,7 +165,7 @@ POST /groups/:id/epics ...@@ -165,7 +165,7 @@ POST /groups/:id/epics
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `title` | string | yes | The title of the epic | | `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma separated list of labels | | `labels` | string | no | The comma separated list of labels |
| `description` | string | no | The description of the epic | | `description` | string | no | The description of the epic. Limited to 1 000 000 characters. |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | | `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) | | `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
...@@ -231,7 +231,7 @@ PUT /groups/:id/epics/:epic_iid ...@@ -231,7 +231,7 @@ PUT /groups/:id/epics/:epic_iid
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `epic_iid` | integer/string | yes | The internal ID of the epic | | `epic_iid` | integer/string | yes | The internal ID of the epic |
| `title` | string | no | The title of an epic | | `title` | string | no | The title of an epic |
| `description` | string | no | The description of an epic | | `description` | string | no | The description of an epic. Limited to 1 000 000 characters. |
| `labels` | string | no | The comma separated list of labels | | `labels` | string | no | The comma separated list of labels |
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) | | `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) | | `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
......
...@@ -590,7 +590,7 @@ POST /projects/:id/issues ...@@ -590,7 +590,7 @@ POST /projects/:id/issues
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) | | `iid` | integer/string | no | The internal ID of the project's issue (requires admin or project owner rights) |
| `title` | string | yes | The title of an issue | | `title` | string | yes | The title of an issue |
| `description` | string | no | The description of an issue | | `description` | string | no | The description of an issue. Limited to 1 000 000 characters. |
| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. | | `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
| `assignee_ids` | integer array | no | The ID of a user to assign issue | | `assignee_ids` | integer array | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The global ID of a milestone to assign issue | | `milestone_id` | integer | no | The global ID of a milestone to assign issue |
...@@ -691,7 +691,7 @@ PUT /projects/:id/issues/:issue_iid ...@@ -691,7 +691,7 @@ PUT /projects/:id/issues/:issue_iid
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of a project's issue | | `issue_iid` | integer | yes | The internal ID of a project's issue |
| `title` | string | no | The title of an issue | | `title` | string | no | The title of an issue |
| `description` | string | no | The description of an issue | | `description` | string | no | The description of an issue. Limited to 1 000 000 characters. |
| `confidential` | boolean | no | Updates an issue to be confidential | | `confidential` | boolean | no | Updates an issue to be confidential |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_ids` | integer array | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| | `milestone_id` | integer | no | The global ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.|
......
...@@ -837,7 +837,7 @@ POST /projects/:id/merge_requests ...@@ -837,7 +837,7 @@ POST /projects/:id/merge_requests
| `title` | string | yes | Title of MR | | `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID | | `assignee_id` | integer | no | Assignee user ID |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR. Limited to 1 000 000 characters. |
| `target_project_id` | integer | no | The target project (numeric id) | | `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list | | `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The global ID of a milestone | | `milestone_id` | integer | no | The global ID of a milestone |
...@@ -990,7 +990,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid ...@@ -990,7 +990,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. | | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR. Limited to 1 000 000 characters. |
| `state_event` | string | no | New state (close/reopen) | | `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
| `squash` | boolean | no | Squash commits into a single commit when merging | | `squash` | boolean | no | Squash commits into a single commit when merging |
......
...@@ -113,7 +113,7 @@ Parameters: ...@@ -113,7 +113,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_iid` (required) - The IID of an issue - `issue_iid` (required) - The IID of an issue
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights)
```bash ```bash
...@@ -133,7 +133,7 @@ Parameters: ...@@ -133,7 +133,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `issue_iid` (required) - The IID of an issue - `issue_iid` (required) - The IID of an issue
- `note_id` (required) - The ID of a note - `note_id` (required) - The ID of a note
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note
...@@ -231,7 +231,7 @@ Parameters: ...@@ -231,7 +231,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `snippet_id` (required) - The ID of a snippet - `snippet_id` (required) - The ID of a snippet
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
```bash ```bash
...@@ -251,7 +251,7 @@ Parameters: ...@@ -251,7 +251,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `snippet_id` (required) - The ID of a snippet - `snippet_id` (required) - The ID of a snippet
- `note_id` (required) - The ID of a note - `note_id` (required) - The ID of a note
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/notes?body=note curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/notes?body=note
...@@ -354,7 +354,7 @@ Parameters: ...@@ -354,7 +354,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The IID of a merge request - `merge_request_iid` (required) - The IID of a merge request
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
### Modify existing merge request note ### Modify existing merge request note
...@@ -370,7 +370,7 @@ Parameters: ...@@ -370,7 +370,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding)
- `merge_request_iid` (required) - The IID of a merge request - `merge_request_iid` (required) - The IID of a merge request
- `note_id` (required) - The ID of a note - `note_id` (required) - The ID of a note
- `body` (required) - The content of a note - `body` (required) - The content of a note. Limited to 1 000 000 characters.
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/notes?body=note
...@@ -472,7 +472,7 @@ Parameters: ...@@ -472,7 +472,7 @@ Parameters:
| --------- | -------------- | -------- | ----------- | | --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `epic_id` | integer | yes | The ID of an epic | | `epic_id` | integer | yes | The ID of an epic |
| `body` | string | yes | The content of a note | | `body` | string | yes | The content of a note. Limited to 1 000 000 characters. |
```bash ```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
...@@ -493,7 +493,7 @@ Parameters: ...@@ -493,7 +493,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `epic_id` | integer | yes | The ID of an epic | | `epic_id` | integer | yes | The ID of an epic |
| `note_id` | integer | yes | The ID of a note | | `note_id` | integer | yes | The ID of a note |
| `body` | string | yes | The content of a note | | `body` | string | yes | The content of a note. Limited to 1 000 000 characters. |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
......
...@@ -68,6 +68,9 @@ Example response: ...@@ -68,6 +68,9 @@ Example response:
"allow_local_requests_from_hooks_and_services": true, "allow_local_requests_from_hooks_and_services": true,
"allow_local_requests_from_web_hooks_and_services": true, "allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false "allow_local_requests_from_system_hooks": false
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"]
} }
``` ```
...@@ -141,6 +144,9 @@ Example response: ...@@ -141,6 +144,9 @@ Example response:
"user_show_add_ssh_key_message": true, "user_show_add_ssh_key_message": true,
"file_template_project_id": 1, "file_template_project_id": 1,
"local_markdown_version": 0, "local_markdown_version": 0,
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"geo_node_allowed_ips": "0.0.0.0/0, ::/0", "geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"allow_local_requests_from_hooks_and_services": true, "allow_local_requests_from_hooks_and_services": true,
"allow_local_requests_from_web_hooks_and_services": true, "allow_local_requests_from_web_hooks_and_services": true,
...@@ -186,6 +192,10 @@ are listed in the descriptions of the relevant settings. ...@@ -186,6 +192,10 @@ are listed in the descriptions of the relevant settings.
| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. | | `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. |
| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. | | `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. |
| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. | | `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
| `asset_proxy_enabled` | boolean | no | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. GitLab restart is required to apply changes. |
| `asset_proxy_secret_key` | string | no | Shared secret with the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_url` | string | no | URL of the asset proxy server. GitLab restart is required to apply changes. |
| `asset_proxy_whitelist` | string or array of strings | no | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. GitLab restart is required to apply changes. |
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. | | `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. | | `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. | | `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
......
...@@ -18,3 +18,4 @@ type: index ...@@ -18,3 +18,4 @@ type: index
- [Enforce Two-factor authentication](two_factor_authentication.md) - [Enforce Two-factor authentication](two_factor_authentication.md)
- [Send email confirmation on sign-up](user_email_confirmation.md) - [Send email confirmation on sign-up](user_email_confirmation.md)
- [Security of running jobs](https://docs.gitlab.com/runner/security/) - [Security of running jobs](https://docs.gitlab.com/runner/security/)
- [Proxying images](asset_proxy.md)
A possible security concern when managing a public facing GitLab instance is
the ability to steal a users IP address by referencing images in issues, comments, etc.
For example, adding `![Example image](http://example.com/example.png)` to
an issue description will cause the image to be loaded from the external
server in order to be displayed. However this also allows the external server
to log the IP address of the user.
One way to mitigate this is by proxying any external images to a server you
control. GitLab handles this by allowing you to run the "Camo" server
[cactus/go-camo](https://github.com/cactus/go-camo#how-it-works).
The image request is sent to the Camo server, which then makes the request for
the original image. This way an attacker only ever seems the IP address
of your Camo server.
Once you have your Camo server up and running, you can configure GitLab to
proxy image requests to it. The following settings are supported:
| Attribute | Description |
| ------------------------ | ----------- |
| `asset_proxy_enabled` | (**If enabled, requires:** `asset_proxy_url`) Enable proxying of assets. |
| `asset_proxy_secret_key` | Shared secret with the asset proxy server. |
| `asset_proxy_url` | URL of the asset proxy server. |
| `asset_proxy_whitelist` | Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted. |
These can be set via the [Application setting API](../api/settings.md)
Note that a GitLab restart is required to apply any changes.
...@@ -239,7 +239,7 @@ module API ...@@ -239,7 +239,7 @@ module API
# because notes are redacted if they point to projects that # because notes are redacted if they point to projects that
# cannot be accessed by the user. # cannot be accessed by the user.
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes.select { |n| n.visible_for?(current_user) }
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
......
...@@ -1174,6 +1174,9 @@ module API ...@@ -1174,6 +1174,9 @@ module API
attributes.delete(:performance_bar_enabled) attributes.delete(:performance_bar_enabled)
attributes.delete(:allow_local_requests_from_hooks_and_services) attributes.delete(:allow_local_requests_from_hooks_and_services)
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
attributes attributes
end end
......
...@@ -12,7 +12,7 @@ module API ...@@ -12,7 +12,7 @@ module API
end end
def update_note(noteable, note_id) def update_note(noteable, note_id)
note = noteable.notes.find(params[:note_id]) note = noteable.notes.find(note_id)
authorize! :admin_note, note authorize! :admin_note, note
...@@ -61,8 +61,8 @@ module API ...@@ -61,8 +61,8 @@ module API
end end
def get_note(noteable, note_id) def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id]) note = noteable.notes.with_metadata.find(note_id)
can_read_note = !note.cross_reference_not_visible_for?(current_user) can_read_note = note.visible_for?(current_user)
if can_read_note if can_read_note
present note, with: Entities::Note present note, with: Entities::Note
......
...@@ -42,7 +42,7 @@ module API ...@@ -42,7 +42,7 @@ module API
# array returned, but this is really a edge-case. # array returned, but this is really a edge-case.
notes = paginate(raw_notes) notes = paginate(raw_notes)
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } notes = notes.select { |note| note.visible_for?(current_user) }
present notes, with: Entities::Note present notes, with: Entities::Note
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -36,6 +36,10 @@ module API ...@@ -36,6 +36,10 @@ module API
given akismet_enabled: ->(val) { val } do given akismet_enabled: ->(val) { val } do
requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com' requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
end end
optional :asset_proxy_enabled, type: Boolean, desc: 'Enable proxying of assets'
optional :asset_proxy_url, type: String, desc: 'URL of the asset proxy server'
optional :asset_proxy_secret_key, type: String, desc: 'Shared secret with the asset proxy server'
optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
...@@ -104,6 +108,11 @@ module API ...@@ -104,6 +108,11 @@ module API
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha' requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
end end
optional :login_recaptcha_protection_enabled, type: Boolean, desc: 'Helps prevent brute-force attacks'
given login_recaptcha_protection_enabled: ->(val) { val } do
requires :recaptcha_site_key, type: String, desc: 'Generate site key at http://www.google.com/recaptcha'
requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
end
optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects' optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication'
...@@ -123,7 +132,7 @@ module API ...@@ -123,7 +132,7 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
given snowplow_enabled: ->(val) { val } do given snowplow_enabled: ->(val) { val } do
......
# frozen_string_literal: true
module API
module Validations
module Types
class CommaSeparatedToArray
def self.coerce
lambda do |value|
case value
when String
value.split(',').map(&:strip)
when Array
value.map { |v| v.to_s.split(',').map(&:strip) }.flatten
else
[]
end
end
end
end
end
end
end
...@@ -7,6 +7,14 @@ module Banzai ...@@ -7,6 +7,14 @@ module Banzai
class AbstractReferenceFilter < ReferenceFilter class AbstractReferenceFilter < ReferenceFilter
include CrossProjectReference include CrossProjectReference
# REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# reference (which we replace with placeholder during re-scaping). The
# random number helps ensure it's pretty close to unique. Since it's a
# transitory value (it never gets saved) we can initialize once, and it
# doesn't matter if it changes on a restart.
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
def self.object_class def self.object_class
# Implement in child class # Implement in child class
# Example: MergeRequest # Example: MergeRequest
...@@ -389,6 +397,14 @@ module Banzai ...@@ -389,6 +397,14 @@ module Banzai
def escape_html_entities(text) def escape_html_entities(text)
CGI.escapeHTML(text.to_s) CGI.escapeHTML(text.to_s)
end end
def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text)
escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
placeholder_data[$1.to_i]
end
end
end end
end end
end end
# frozen_string_literal: true
module Banzai
module Filter
# Proxy's images/assets to another server. Reduces mixed content warnings
# as well as hiding the customer's IP address when requesting images.
# Copies the original img `src` to `data-canonical-src` then replaces the
# `src` with a new url to the proxy server.
class AssetProxyFilter < HTML::Pipeline::CamoFilter
def initialize(text, context = nil, result = nil)
super
end
def validate
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
end
def asset_host_whitelisted?(host)
context[:asset_proxy_domain_regexp] ? context[:asset_proxy_domain_regexp].match?(host) : false
end
def self.transform_context(context)
context[:disable_asset_proxy] = !Gitlab.config.asset_proxy.enabled
unless context[:disable_asset_proxy]
context[:asset_proxy_enabled] = !context[:disable_asset_proxy]
context[:asset_proxy] = Gitlab.config.asset_proxy.url
context[:asset_proxy_secret_key] = Gitlab.config.asset_proxy.secret_key
context[:asset_proxy_domain_regexp] = Gitlab.config.asset_proxy.domain_regexp
end
context
end
# called during an initializer. Caching the values in Gitlab.config significantly increased
# performance, rather than querying Gitlab::CurrentSettings.current_application_settings
# over and over. However, this does mean that the Rails servers need to get restarted
# whenever the application settings are changed
def self.initialize_settings
application_settings = Gitlab::CurrentSettings.current_application_settings
Gitlab.config['asset_proxy'] ||= Settingslogic.new({})
if application_settings.respond_to?(:asset_proxy_enabled)
Gitlab.config.asset_proxy['enabled'] = application_settings.asset_proxy_enabled
Gitlab.config.asset_proxy['url'] = application_settings.asset_proxy_url
Gitlab.config.asset_proxy['secret_key'] = application_settings.asset_proxy_secret_key
Gitlab.config.asset_proxy['whitelist'] = application_settings.asset_proxy_whitelist || [Gitlab.config.gitlab.host]
Gitlab.config.asset_proxy['domain_regexp'] = compile_whitelist(Gitlab.config.asset_proxy.whitelist)
else
Gitlab.config.asset_proxy['enabled'] = ::ApplicationSetting.defaults[:asset_proxy_enabled]
end
end
def self.compile_whitelist(domain_list)
return if domain_list.empty?
escaped = domain_list.map { |domain| Regexp.escape(domain).gsub('\*', '.*?') }
Regexp.new("^(#{escaped.join('|')})$", Regexp::IGNORECASE)
end
end
end
end
...@@ -14,10 +14,10 @@ module Banzai ...@@ -14,10 +14,10 @@ module Banzai
# such as on `mailto:` links. Since we've been using it, do an # such as on `mailto:` links. Since we've been using it, do an
# initial parse for validity and then use Addressable # initial parse for validity and then use Addressable
# for IDN support, etc # for IDN support, etc
uri = uri_strict(node['href'].to_s) uri = uri_strict(node_src(node))
if uri if uri
node.set_attribute('href', uri.to_s) node.set_attribute(node_src_attribute(node), uri.to_s)
addressable_uri = addressable_uri(node['href']) addressable_uri = addressable_uri(node_src(node))
else else
addressable_uri = nil addressable_uri = nil
end end
...@@ -35,6 +35,16 @@ module Banzai ...@@ -35,6 +35,16 @@ module Banzai
private private
# if this is a link to a proxied image, then `src` is already the correct
# proxied url, so work with the `data-canonical-src`
def node_src_attribute(node)
node['data-canonical-src'] ? 'data-canonical-src' : 'href'
end
def node_src(node)
node[node_src_attribute(node)]
end
def uri_strict(href) def uri_strict(href)
URI.parse(href) URI.parse(href)
rescue URI::Error rescue URI::Error
...@@ -72,7 +82,7 @@ module Banzai ...@@ -72,7 +82,7 @@ module Banzai
return unless uri return unless uri
return unless context[:emailable_links] return unless context[:emailable_links]
unencoded_uri_str = Addressable::URI.unencode(node['href']) unencoded_uri_str = Addressable::URI.unencode(node_src(node))
if unencoded_uri_str == node.content && idn?(uri) if unencoded_uri_str == node.content && idn?(uri)
node.content = uri.normalize node.content = uri.normalize
......
...@@ -18,6 +18,9 @@ module Banzai ...@@ -18,6 +18,9 @@ module Banzai
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
) )
# make sure the original non-proxied src carries over to the link
link['data-canonical-src'] = img['data-canonical-src'] if img['data-canonical-src']
link.children = img.clone link.children = img.clone
img.replace(link) img.replace(link)
......
...@@ -14,24 +14,24 @@ module Banzai ...@@ -14,24 +14,24 @@ module Banzai
find_labels(parent_object).find(id) find_labels(parent_object).find(id)
end end
def self.references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match|
yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~
end
end
def references_in(text, pattern = Label.reference_pattern) def references_in(text, pattern = Label.reference_pattern)
unescape_html_entities(text).gsub(pattern) do |match| labels = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace, project = $~[:namespace], $~[:project] namespace, project = $~[:namespace], $~[:project]
project_path = full_project_path(namespace, project) project_path = full_project_path(namespace, project)
label = find_label(project_path, $~[:label_id], $~[:label_name]) label = find_label(project_path, $~[:label_id], $~[:label_name])
if label if label
yield match, label.id, project, namespace, $~ labels[label.id] = yield match, label.id, project, namespace, $~
"#{REFERENCE_PLACEHOLDER}#{label.id}"
else else
escape_html_entities(match) match
end end
end end
return text if labels.empty?
escape_with_placeholders(unescaped_html, labels)
end end
def find_label(parent_ref, label_id, label_name) def find_label(parent_ref, label_id, label_name)
......
...@@ -51,15 +51,21 @@ module Banzai ...@@ -51,15 +51,21 @@ module Banzai
# default implementation. # default implementation.
return super(text, pattern) if pattern != Milestone.reference_pattern return super(text, pattern) if pattern != Milestone.reference_pattern
unescape_html_entities(text).gsub(pattern) do |match| milestones = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone if milestone
yield match, milestone.id, $~[:project], $~[:namespace], $~ milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
"#{REFERENCE_PLACEHOLDER}#{milestone.id}"
else else
escape_html_entities(match) match
end end
end end
return text if milestones.empty?
escape_with_placeholders(unescaped_html, milestones)
end end
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
......
...@@ -9,6 +9,7 @@ module Banzai ...@@ -9,6 +9,7 @@ module Banzai
# Context options: # Context options:
# :commit # :commit
# :group # :group
# :current_user
# :project # :project
# :project_wiki # :project_wiki
# :ref # :ref
...@@ -18,6 +19,7 @@ module Banzai ...@@ -18,6 +19,7 @@ module Banzai
def call def call
return doc if context[:system_note] return doc if context[:system_note]
return doc unless visible_to_user?
@uri_types = {} @uri_types = {}
clear_memoization(:linkable_files) clear_memoization(:linkable_files)
...@@ -166,6 +168,16 @@ module Banzai ...@@ -166,6 +168,16 @@ module Banzai
Gitlab.config.gitlab.relative_url_root.presence || '/' Gitlab.config.gitlab.relative_url_root.presence || '/'
end end
def visible_to_user?
if project
Ability.allowed?(current_user, :download_code, project)
elsif group
Ability.allowed?(current_user, :read_group, group)
else # Objects detached from projects or groups, e.g. Personal Snippets.
true
end
end
def ref def ref
context[:ref] || project.default_branch context[:ref] || project.default_branch
end end
...@@ -178,6 +190,10 @@ module Banzai ...@@ -178,6 +190,10 @@ module Banzai
context[:project] context[:project]
end end
def current_user
context[:current_user]
end
def repository def repository
@repository ||= project&.repository @repository ||= project&.repository
end end
......
...@@ -23,6 +23,14 @@ module Banzai ...@@ -23,6 +23,14 @@ module Banzai
"'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})" "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})"
end end
if context[:asset_proxy_enabled].present?
src_query.concat(
UploaderHelper::VIDEO_EXT.map do |ext|
"'.#{ext}' = substring(@data-canonical-src, string-length(@data-canonical-src) - #{ext.size})"
end
)
end
"descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]" "descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]"
end end
end end
...@@ -48,6 +56,13 @@ module Banzai ...@@ -48,6 +56,13 @@ module Banzai
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'") title: "Download '#{element['title'] || element['alt']}'")
# make sure the original non-proxied src carries over
if element['data-canonical-src']
video['data-canonical-src'] = element['data-canonical-src']
link['data-canonical-src'] = element['data-canonical-src']
end
download_paragraph = doc.document.create_element('p') download_paragraph = doc.document.create_element('p')
download_paragraph.children = link download_paragraph.children = link
......
...@@ -6,12 +6,17 @@ module Banzai ...@@ -6,12 +6,17 @@ module Banzai
def self.filters def self.filters
FilterArray[ FilterArray[
Filter::AsciiDocSanitizationFilter, Filter::AsciiDocSanitizationFilter,
Filter::AssetProxyFilter,
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter, Filter::ExternalLinkFilter,
Filter::PlantumlFilter, Filter::PlantumlFilter,
Filter::AsciiDocPostProcessingFilter Filter::AsciiDocPostProcessingFilter
] ]
end end
def self.transform_context(context)
Filter::AssetProxyFilter.transform_context(context)
end
end end
end end
end end
...@@ -17,6 +17,7 @@ module Banzai ...@@ -17,6 +17,7 @@ module Banzai
Filter::SpacedLinkFilter, Filter::SpacedLinkFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::AssetProxyFilter,
Filter::SyntaxHighlightFilter, Filter::SyntaxHighlightFilter,
Filter::MathFilter, Filter::MathFilter,
...@@ -60,7 +61,7 @@ module Banzai ...@@ -60,7 +61,7 @@ module Banzai
def self.transform_context(context) def self.transform_context(context)
context[:only_path] = true unless context.key?(:only_path) context[:only_path] = true unless context.key?(:only_path)
context Filter::AssetProxyFilter.transform_context(context)
end end
end end
end end
......
...@@ -6,11 +6,16 @@ module Banzai ...@@ -6,11 +6,16 @@ module Banzai
def self.filters def self.filters
@filters ||= FilterArray[ @filters ||= FilterArray[
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::AssetProxyFilter,
Filter::ExternalLinkFilter, Filter::ExternalLinkFilter,
Filter::PlantumlFilter, Filter::PlantumlFilter,
Filter::SyntaxHighlightFilter Filter::SyntaxHighlightFilter
] ]
end end
def self.transform_context(context)
Filter::AssetProxyFilter.transform_context(context)
end
end end
end end
end end
...@@ -7,6 +7,7 @@ module Banzai ...@@ -7,6 +7,7 @@ module Banzai
@filters ||= FilterArray[ @filters ||= FilterArray[
Filter::HtmlEntityFilter, Filter::HtmlEntityFilter,
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::AssetProxyFilter,
Filter::EmojiFilter, Filter::EmojiFilter,
Filter::AutolinkFilter, Filter::AutolinkFilter,
...@@ -29,6 +30,8 @@ module Banzai ...@@ -29,6 +30,8 @@ module Banzai
end end
def self.transform_context(context) def self.transform_context(context)
context = Filter::AssetProxyFilter.transform_context(context)
super(context).merge( super(context).merge(
no_sourcepos: true no_sourcepos: true
) )
......
# frozen_string_literal: true
module Gitlab
class AnonymousSession
def initialize(remote_ip, session_id: nil)
@remote_ip = remote_ip
@session_id = session_id
end
def store_session_id_per_ip
Gitlab::Redis::SharedState.with do |redis|
redis.pipelined do
redis.sadd(session_lookup_name, session_id)
redis.expire(session_lookup_name, 24.hours)
end
end
end
def stored_sessions
Gitlab::Redis::SharedState.with do |redis|
redis.scard(session_lookup_name)
end
end
def cleanup_session_per_ip_entries
Gitlab::Redis::SharedState.with do |redis|
redis.srem(session_lookup_name, session_id)
end
end
private
attr_reader :remote_ip, :session_id
def session_lookup_name
@session_lookup_name ||= "#{Gitlab::Redis::SharedState::IP_SESSIONS_LOOKUP_NAMESPACE}:#{remote_ip}"
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
module Limit
class JobActivity < Chain::Base
def perform!
# to be overridden in EE
end
def break?
false # to be overridden in EE
end
end
end
end
end
end
end
...@@ -13,6 +13,10 @@ module Gitlab ...@@ -13,6 +13,10 @@ module Gitlab
# FIXME: this should just be the max value of timestampz # FIXME: this should just be the max value of timestampz
MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze
# The maximum number of characters for text fields, to avoid DoS attacks via parsing huge text fields
# https://gitlab.com/gitlab-org/gitlab-ce/issues/61974
MAX_TEXT_SIZE_LIMIT = 1_000_000
# Minimum schema version from which migrations are supported # Minimum schema version from which migrations are supported
# Migrations before this version may have been removed # Migrations before this version may have been removed
MIN_SCHEMA_VERSION = 20190506135400 MIN_SCHEMA_VERSION = 20190506135400
......
# frozen_string_literal: true
module Gitlab
module Jira
# Gitlab JIRA HTTP client to be used with jira-ruby gem, this subclasses JIRA::HTTPClient.
# Uses Gitlab::HTTP to make requests to JIRA REST API.
# The parent class implementation can be found at: https://github.com/sumoheavy/jira-ruby/blob/v1.4.0/lib/jira/http_client.rb
class HttpClient < JIRA::HttpClient
extend ::Gitlab::Utils::Override
override :request
def request(*args)
result = make_request(*args)
raise JIRA::HTTPError.new(result) unless result.response.is_a?(Net::HTTPSuccess)
result
end
override :make_cookie_auth_request
def make_cookie_auth_request
body = {
username: @options.delete(:username),
password: @options.delete(:password)
}.to_json
make_request(:post, @options[:context_path] + '/rest/auth/1/session', body, { 'Content-Type' => 'application/json' })
end
override :make_request
def make_request(http_method, path, body = '', headers = {})
request_params = { headers: headers }
request_params[:body] = body if body.present?
request_params[:headers][:Cookie] = get_cookies if options[:use_cookies]
request_params[:timeout] = options[:read_timeout] if options[:read_timeout]
request_params[:base_uri] = uri.to_s
request_params.merge!(auth_params)
result = Gitlab::HTTP.public_send(http_method, path, **request_params) # rubocop:disable GitlabSecurity/PublicSend
@authenticated = result.response.is_a?(Net::HTTPOK)
store_cookies(result) if options[:use_cookies]
result
end
def auth_params
return {} unless @options[:username] && @options[:password]
{
basic_auth: {
username: @options[:username],
password: @options[:password]
}
}
end
private
def get_cookies
cookie_array = @cookies.values.map { |cookie| "#{cookie.name}=#{cookie.value[0]}" }
cookie_array += Array(@options[:additional_cookies]) if @options.key?(:additional_cookies)
cookie_array.join('; ') if cookie_array.any?
end
end
end
end
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
module Gitlab module Gitlab
module MarkdownCache module MarkdownCache
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 17
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
CACHE_COMMONMARK_VERSION = 16
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)
UnsupportedClassError = Class.new(BaseError) UnsupportedClassError = Class.new(BaseError)
......
...@@ -132,7 +132,7 @@ module Gitlab ...@@ -132,7 +132,7 @@ module Gitlab
NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze
NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze NAMESPACE_FORMAT_REGEX = /(?:#{NAMESPACE_FORMAT_REGEX_JS})#{NO_SUFFIX_REGEX}/.freeze
PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze PROJECT_PATH_FORMAT_REGEX = /(?:#{PATH_REGEX_STR})#{NO_SUFFIX_REGEX}/.freeze
FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/)*#{NAMESPACE_FORMAT_REGEX}}.freeze FULL_NAMESPACE_FORMAT_REGEX = %r{(#{NAMESPACE_FORMAT_REGEX}/){,#{Namespace::NUMBER_OF_ANCESTORS_ALLOWED}}#{NAMESPACE_FORMAT_REGEX}}.freeze
def root_namespace_route_regex def root_namespace_route_regex
@root_namespace_route_regex ||= begin @root_namespace_route_regex ||= begin
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module Recaptcha module Recaptcha
def self.load_configurations! def self.load_configurations!
if Gitlab::CurrentSettings.recaptcha_enabled if Gitlab::CurrentSettings.recaptcha_enabled || enabled_on_login?
::Recaptcha.configure do |config| ::Recaptcha.configure do |config|
config.site_key = Gitlab::CurrentSettings.recaptcha_site_key config.site_key = Gitlab::CurrentSettings.recaptcha_site_key
config.secret_key = Gitlab::CurrentSettings.recaptcha_private_key config.secret_key = Gitlab::CurrentSettings.recaptcha_private_key
...@@ -16,5 +16,9 @@ module Gitlab ...@@ -16,5 +16,9 @@ module Gitlab
def self.enabled? def self.enabled?
Gitlab::CurrentSettings.recaptcha_enabled Gitlab::CurrentSettings.recaptcha_enabled
end end
def self.enabled_on_login?
Gitlab::CurrentSettings.login_recaptcha_protection_enabled
end
end end
end end
...@@ -9,6 +9,7 @@ module Gitlab ...@@ -9,6 +9,7 @@ module Gitlab
SESSION_NAMESPACE = 'session:gitlab'.freeze SESSION_NAMESPACE = 'session:gitlab'.freeze
USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze
USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze
IP_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:ip:gitlab'.freeze
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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