diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-25 17:28:47 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-25 17:28:47 +0000 |
commit | 7843757fac081350dd74c3fb50d32b66ae8ab16f (patch) | |
tree | 35d7f1bdc90a138aaebe5e39e96e644a924b97b2 | |
parent | cc6190d962e3050b0b16b6d0958d9227187d93ce (diff) | |
download | gitlab-ce-7843757fac081350dd74c3fb50d32b66ae8ab16f.tar.gz |
Add latest changes from gitlab-org/security/gitlab@12-9-stable-ee
-rw-r--r-- | app/controllers/concerns/hotlink_interceptor.rb | 15 | ||||
-rw-r--r-- | app/controllers/projects/repositories_controller.rb | 2 | ||||
-rw-r--r-- | changelogs/unreleased/security-repository-archive-hotlinking.yml | 5 | ||||
-rw-r--r-- | lib/api/helpers.rb | 4 | ||||
-rw-r--r-- | lib/api/repositories.rb | 2 | ||||
-rw-r--r-- | lib/gitlab/hotlinking_detector.rb | 52 | ||||
-rw-r--r-- | spec/controllers/projects/repositories_controller_spec.rb | 6 | ||||
-rw-r--r-- | spec/lib/gitlab/hotlinking_detector_spec.rb | 75 | ||||
-rw-r--r-- | spec/requests/api/repositories_spec.rb | 12 | ||||
-rw-r--r-- | spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb | 87 | ||||
-rwxr-xr-x[-rw-r--r--] | vendor/gitignore/C++.gitignore | 0 | ||||
-rwxr-xr-x[-rw-r--r--] | vendor/gitignore/Java.gitignore | 0 |
12 files changed, 260 insertions, 0 deletions
diff --git a/app/controllers/concerns/hotlink_interceptor.rb b/app/controllers/concerns/hotlink_interceptor.rb new file mode 100644 index 00000000000..712a10eab98 --- /dev/null +++ b/app/controllers/concerns/hotlink_interceptor.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module HotlinkInterceptor + extend ActiveSupport::Concern + + def intercept_hotlinking! + return render_406 if Gitlab::HotlinkingDetector.intercept_hotlinking?(request) + end + + private + + def render_406 + head :not_acceptable + end +end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 1cb9e1d2c9b..303326bbe23 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -4,12 +4,14 @@ class Projects::RepositoriesController < Projects::ApplicationController include ExtractsPath include StaticObjectExternalStorage include Gitlab::RateLimitHelpers + include HotlinkInterceptor prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) } # Authorize before_action :require_non_empty_project, except: :create before_action :archive_rate_limit!, only: :archive + before_action :intercept_hotlinking!, only: :archive before_action :assign_archive_vars, only: :archive before_action :assign_append_sha, only: :archive before_action :authorize_download_code! diff --git a/changelogs/unreleased/security-repository-archive-hotlinking.yml b/changelogs/unreleased/security-repository-archive-hotlinking.yml new file mode 100644 index 00000000000..cf87ea488f0 --- /dev/null +++ b/changelogs/unreleased/security-repository-archive-hotlinking.yml @@ -0,0 +1,5 @@ +--- +title: Block hotlinking to repository archives +merge_request: +author: +type: security diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c3b5654e217..615b1b0eb7f 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -367,6 +367,10 @@ module API render_api_error!('405 Method Not Allowed', 405) end + def not_acceptable! + render_api_error!('406 Not Acceptable', 406) + end + def service_unavailable! render_api_error!('503 Service Unavailable', 503) end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 62f5b67af1e..0b2df85f61f 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -95,6 +95,8 @@ module API render_api_error!({ error: ::Gitlab::RateLimitHelpers::ARCHIVE_RATE_LIMIT_REACHED_MESSAGE }, 429) end + not_acceptable! if Gitlab::HotlinkingDetector.intercept_hotlinking?(request) + send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true rescue not_found!('File') diff --git a/lib/gitlab/hotlinking_detector.rb b/lib/gitlab/hotlinking_detector.rb new file mode 100644 index 00000000000..44901297870 --- /dev/null +++ b/lib/gitlab/hotlinking_detector.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Gitlab + class HotlinkingDetector + IMAGE_FORMATS = %w(image/jpeg image/apng image/png image/webp image/svg+xml image/*).freeze + MEDIA_FORMATS = %w(video/webm video/ogg video/* application/ogg audio/webm audio/ogg audio/wav audio/*).freeze + CSS_FORMATS = %w(text/css).freeze + INVALID_FORMATS = (IMAGE_FORMATS + MEDIA_FORMATS + CSS_FORMATS).freeze + INVALID_FETCH_MODES = %w(cors no-cors websocket).freeze + + class << self + def intercept_hotlinking?(request) + request_accepts = parse_request_accepts(request) + + return false unless Feature.enabled?(:repository_archive_hotlinking_interception, default_enabled: true) + + # Block attempts to embed as JS + return true if sec_fetch_invalid?(request) + + # If no Accept header was set, skip the rest + return false if request_accepts.empty? + + # Workaround for IE8 weirdness + return false if IMAGE_FORMATS.include?(request_accepts.first) && request_accepts.include?("application/x-ms-application") + + # Block all other media requests if the first format is a media type + return true if INVALID_FORMATS.include?(request_accepts.first) + + false + end + + private + + def sec_fetch_invalid?(request) + fetch_mode = request.headers["Sec-Fetch-Mode"] + + return if fetch_mode.blank? + return true if INVALID_FETCH_MODES.include?(fetch_mode) + end + + def parse_request_accepts(request) + # Rails will already have parsed the Accept header + return request.accepts if request.respond_to?(:accepts) + + # Grape doesn't parse it, so we can use the Rails system for this + return Mime::Type.parse(request.headers["Accept"]) if request.respond_to?(:headers) && request.headers["Accept"].present? + + [] + end + end + end +end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 2d39f0afaee..42032b4cad0 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -28,6 +28,12 @@ describe Projects::RepositoriesController do sign_in(user) end + it_behaves_like "hotlink interceptor" do + let(:http_request) do + get :archive, params: { namespace_id: project.namespace, project_id: project, id: "master" }, format: "zip" + end + end + it "uses Gitlab::Workhorse" do get :archive, params: { namespace_id: project.namespace, project_id: project, id: "master" }, format: "zip" diff --git a/spec/lib/gitlab/hotlinking_detector_spec.rb b/spec/lib/gitlab/hotlinking_detector_spec.rb new file mode 100644 index 00000000000..536d744c197 --- /dev/null +++ b/spec/lib/gitlab/hotlinking_detector_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::HotlinkingDetector do + describe ".intercept_hotlinking?" do + using RSpec::Parameterized::TableSyntax + + subject { described_class.intercept_hotlinking?(request) } + + let(:request) { double("request", headers: headers) } + let(:headers) { {} } + + context "hotlinked as media" do + where(:return_value, :accept_header) do + # These are default formats in modern browsers, and IE + false | "*/*" + false | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + false | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + false | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + false | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + false | "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*" + false | "text/html, application/xhtml+xml, image/jxr, */*" + false | "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1" + + # These are image request formats + true | "image/webp,*/*" + true | "image/png,image/*;q=0.8,*/*;q=0.5" + true | "image/webp,image/apng,image/*,*/*;q=0.8" + true | "image/png,image/svg+xml,image/*;q=0.8, */*;q=0.5" + + # Video request formats + true | "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5" + + # Audio request formats + true | "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5" + + # CSS request formats + true | "text/css,*/*;q=0.1" + true | "text/css" + true | "text/css,*/*;q=0.1" + end + + with_them do + let(:headers) do + { "Accept" => accept_header } + end + + it { is_expected.to be(return_value) } + end + end + + context "hotlinked as a script" do + where(:return_value, :fetch_mode) do + # Standard navigation fetch modes + false | "navigate" + false | "nested-navigate" + false | "same-origin" + + # Fetch modes when linking as JS + true | "cors" + true | "no-cors" + true | "websocket" + end + + with_them do + let(:headers) do + { "Sec-Fetch-Mode" => fetch_mode } + end + + it { is_expected.to be(return_value) } + end + end + end +end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 97dc3899d3f..b503c923037 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -275,6 +275,18 @@ describe API::Repositories do expect(response).to have_gitlab_http_status(:too_many_requests) end + + context "when hotlinking detection is enabled" do + before do + Feature.enable(:repository_archive_hotlinking_interception) + end + + it_behaves_like "hotlink interceptor" do + let(:http_request) do + get api(route, current_user), headers: headers + end + end + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb b/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb new file mode 100644 index 00000000000..93a394387a3 --- /dev/null +++ b/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +RSpec.shared_examples "hotlink interceptor" do + let(:http_request) { nil } + let(:headers) { nil } + + describe "DDOS prevention" do + using RSpec::Parameterized::TableSyntax + + context "hotlinked as media" do + where(:response_status, :accept_header) do + # These are default formats in modern browsers, and IE + :ok | "*/*" + :ok | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + :ok | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" + :ok | "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + :ok | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" + :ok | "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*" + :ok | "text/html, application/xhtml+xml, image/jxr, */*" + :ok | "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1" + + # These are image request formats + :not_acceptable | "image/webp,*/*" + :not_acceptable | "image/png,image/*;q=0.8,*/*;q=0.5" + :not_acceptable | "image/webp,image/apng,image/*,*/*;q=0.8" + :not_acceptable | "image/png,image/svg+xml,image/*;q=0.8, */*;q=0.5" + + # Video request formats + :not_acceptable | "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5" + + # Audio request formats + :not_acceptable | "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5" + + # CSS request formats + :not_acceptable | "text/css,*/*;q=0.1" + :not_acceptable | "text/css" + :not_acceptable | "text/css,*/*;q=0.1" + end + + with_them do + let(:headers) do + { "Accept" => accept_header } + end + + before do + request.headers.merge!(headers) if request.present? + end + + it "renders the response" do + http_request + + expect(response).to have_gitlab_http_status(response_status) + end + end + end + + context "hotlinked as a script" do + where(:response_status, :fetch_mode) do + # Standard navigation fetch modes + :ok | "navigate" + :ok | "nested-navigate" + :ok | "same-origin" + + # Fetch modes when linking as JS + :not_acceptable | "cors" + :not_acceptable | "no-cors" + :not_acceptable | "websocket" + end + + with_them do + let(:headers) do + { "Sec-Fetch-Mode" => fetch_mode } + end + + before do + request.headers.merge!(headers) if request.present? + end + + it "renders the response" do + http_request + + expect(response).to have_gitlab_http_status(response_status) + end + end + end + end +end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100644..100755 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100644..100755 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |