Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/url_blocker.rb')
-rw-r--r--lib/gitlab/url_blocker.rb150
1 files changed, 120 insertions, 30 deletions
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 00e609511f2..ba50a42cd37 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -7,20 +7,40 @@ module Gitlab
class UrlBlocker
BlockedUrlError = Class.new(StandardError)
+ DENY_ALL_REQUESTS_EXCEPT_ALLOWED_DEFAULT = proc { deny_all_requests_except_allowed_app_setting }.freeze
+
+ # Result stores the validation result:
+ # uri - The original URI requested
+ # hostname - The hostname that should be used to connect. For DNS
+ # rebinding protection, this will be the resolved IP address of
+ # the hostname.
+ # use_proxy -
+ # If true, this means that the proxy server specified in the
+ # http_proxy/https_proxy environment variables should be used.
+ #
+ # If false, this either means that no proxy server was specified
+ # or that the hostname in the URL is exempt via the no_proxy
+ # environment variable. This allows the caller to disable usage
+ # of a proxy since the IP address may be used to
+ # connect. Otherwise, Net::HTTP may erroneously compare the IP
+ # address against the no_proxy list.
+ Result = Struct.new(:uri, :hostname, :use_proxy)
+
class << self
# Validates the given url according to the constraints specified by arguments.
#
- # ports - Raises error if the given URL port does is not between given ports.
+ # ports - Raises error if the given URL port is not between given ports.
# allow_localhost - Raises error if URL resolves to a localhost IP address and argument is false.
# allow_local_network - Raises error if URL resolves to a link-local address and argument is false.
# allow_object_storage - Avoid raising an error if URL resolves to an object storage endpoint and argument is true.
# ascii_only - Raises error if URL has unicode characters and argument is true.
# enforce_user - Raises error if URL user doesn't start with alphanumeric characters and argument is true.
# enforce_sanitization - Raises error if URL includes any HTML/CSS/JS tags and argument is true.
+ # deny_all_requests_except_allowed - Raises error if URL is not in the allow list and argument is true. Can be Boolean or Proc. Defaults to instance app setting.
#
- # Returns an array with [<uri>, <original-hostname>].
+ # Returns a Result object.
# rubocop:disable Metrics/ParameterLists
- def validate!(
+ def validate_url_with_proxy!(
url,
schemes:,
ports: [],
@@ -30,10 +50,11 @@ module Gitlab
ascii_only: false,
enforce_user: false,
enforce_sanitization: false,
+ deny_all_requests_except_allowed: DENY_ALL_REQUESTS_EXCEPT_ALLOWED_DEFAULT,
dns_rebind_protection: true)
# rubocop:enable Metrics/ParameterLists
- return [nil, nil] if url.nil?
+ return Result.new(nil, nil, true) if url.nil?
raise ArgumentError, 'The schemes is a required argument' if schemes.blank?
@@ -49,21 +70,35 @@ module Gitlab
ascii_only: ascii_only
)
- address_info = get_address_info(uri, dns_rebind_protection)
- return [uri, nil] unless address_info
+ begin
+ address_info = get_address_info(uri)
+ rescue SocketError
+ proxy_in_use = uri_under_proxy_setting?(uri, nil)
+
+ return Result.new(uri, nil, proxy_in_use) unless enforce_address_info_retrievable?(uri, dns_rebind_protection, deny_all_requests_except_allowed)
+
+ raise BlockedUrlError, 'Host cannot be resolved or invalid'
+ end
ip_address = ip_address(address_info)
- return [uri, nil] if domain_allowed?(uri)
+ proxy_in_use = uri_under_proxy_setting?(uri, ip_address)
+
+ # Ignore DNS rebind protection when a proxy is being used, as DNS
+ # rebinding is expected behavior.
+ dns_rebind_protection &&= !proxy_in_use
+ return Result.new(uri, nil, proxy_in_use) if domain_in_allow_list?(uri)
- protected_uri_with_hostname = enforce_uri_hostname(ip_address, uri, dns_rebind_protection)
+ protected_uri_with_hostname = enforce_uri_hostname(ip_address, uri, dns_rebind_protection, proxy_in_use)
- return protected_uri_with_hostname if ip_allowed?(ip_address, port: get_port(uri))
+ return protected_uri_with_hostname if ip_in_allow_list?(ip_address, port: get_port(uri))
# Allow url from the GitLab instance itself but only for the configured hostname and ports
return protected_uri_with_hostname if internal?(uri)
return protected_uri_with_hostname if allow_object_storage && object_storage_endpoint?(uri)
+ validate_deny_all_requests_except_allowed!(deny_all_requests_except_allowed)
+
validate_local_request(
address_info: address_info,
allow_localhost: allow_localhost,
@@ -81,6 +116,13 @@ module Gitlab
true
end
+ # For backwards compatibility, Returns an array with [<uri>, <original-hostname>].
+ # Issue for refactoring: https://gitlab.com/gitlab-org/gitlab/-/issues/410890
+ def validate!(...)
+ result = validate_url_with_proxy!(...)
+ [result.uri, result.hostname]
+ end
+
private
# Returns the given URI with IP address as hostname and the original hostname respectively
@@ -91,12 +133,12 @@ module Gitlab
#
# The original hostname is used to validate the SSL, given in that scenario
# we'll be making the request to the IP address, instead of using the hostname.
- def enforce_uri_hostname(ip_address, uri, dns_rebind_protection)
- return [uri, nil] unless dns_rebind_protection && ip_address && ip_address != uri.hostname
+ def enforce_uri_hostname(ip_address, uri, dns_rebind_protection, proxy_in_use)
+ return Result.new(uri, nil, proxy_in_use) unless dns_rebind_protection && ip_address && ip_address != uri.hostname
new_uri = uri.dup
new_uri.hostname = ip_address
- [new_uri, uri.hostname]
+ Result.new(new_uri, uri.hostname, proxy_in_use)
end
def ip_address(address_info)
@@ -115,29 +157,56 @@ module Gitlab
validate_unicode_restriction(uri) if ascii_only
end
- def get_address_info(uri, dns_rebind_protection)
+ def uri_under_proxy_setting?(uri, ip_address)
+ return false unless Gitlab.http_proxy_env?
+ # `no_proxy|NO_PROXY` specifies addresses for which the proxy is not
+ # used. If it's empty, there are no exceptions and this URI
+ # will be under proxy settings.
+ return true if no_proxy_env.blank?
+
+ # `no_proxy|NO_PROXY` is being used. We must check whether it
+ # applies to this specific URI.
+ ::URI::Generic.use_proxy?(uri.hostname, ip_address, get_port(uri), no_proxy_env)
+ end
+
+ # Returns addrinfo object for the URI.
+ #
+ # @param uri [Addressable::URI]
+ #
+ # @raise [Gitlab::UrlBlocker::BlockedUrlError, ArgumentError] - BlockedUrlError raised if host is too long.
+ #
+ # @return [Array<Addrinfo>]
+ def get_address_info(uri)
Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr|
addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
end
- rescue SocketError
- # If the dns rebinding protection is not enabled or the domain
- # is allowed we avoid the dns rebinding checks
- return if domain_allowed?(uri) || !dns_rebind_protection
+ rescue ArgumentError => error
+ # Addrinfo.getaddrinfo errors if the domain exceeds 1024 characters.
+ raise unless error.message.include?('hostname too long')
+
+ raise BlockedUrlError, "Host is too long (maximum is 1024 characters)"
+ end
+
+ def enforce_address_info_retrievable?(uri, dns_rebind_protection, deny_all_requests_except_allowed)
+ # Do not enforce if URI is in the allow list
+ return false if domain_in_allow_list?(uri)
+
+ # Enforce if the instance should block requests
+ return true if deny_all_requests_except_allowed?(deny_all_requests_except_allowed)
+
+ # Do not enforce if DNS rebinding protection is disabled
+ return false unless dns_rebind_protection
+
+ # Do not enforce if proxy is used
+ return false if Gitlab.http_proxy_env?
# In the test suite we use a lot of mocked urls that are either invalid or
# don't exist. In order to avoid modifying a ton of tests and factories
# we allow invalid urls unless the environment variable RSPEC_ALLOW_INVALID_URLS
# is not true
- return if Rails.env.test? && ENV['RSPEC_ALLOW_INVALID_URLS'] == 'true'
-
- # If the addr can't be resolved or the url is invalid (i.e http://1.1.1.1.1)
- # we block the url
- raise BlockedUrlError, "Host cannot be resolved or invalid"
- rescue ArgumentError => error
- # Addrinfo.getaddrinfo errors if the domain exceeds 1024 characters.
- raise unless error.message.include?('hostname too long')
+ return false if Rails.env.test? && ENV['RSPEC_ALLOW_INVALID_URLS'] == 'true'
- raise BlockedUrlError, "Host is too long (maximum is 1024 characters)"
+ true
end
def validate_local_request(
@@ -260,6 +329,15 @@ module Gitlab
raise BlockedUrlError, "Requests to the link local network are not allowed"
end
+ # Raises a BlockedUrlError if the instance is configured to deny all requests.
+ #
+ # This should only be called after allow list checks have been made.
+ def validate_deny_all_requests_except_allowed!(should_deny)
+ return unless deny_all_requests_except_allowed?(should_deny)
+
+ raise BlockedUrlError, "Requests to hosts and IP addresses not on the Allow List are denied"
+ end
+
# Raises a BlockedUrlError if any IP in `addrs_info` is the limited
# broadcast address.
# https://datatracker.ietf.org/doc/html/rfc919#section-7
@@ -293,8 +371,7 @@ module Gitlab
next unless section_setting && section_setting['enabled']
- # Use #to_h to avoid Settingslogic bug: https://gitlab.com/gitlab-org/gitlab/-/issues/286873
- object_store_setting = section_setting['object_store']&.to_h
+ object_store_setting = section_setting['object_store']
next unless object_store_setting && object_store_setting['enabled']
@@ -302,6 +379,15 @@ module Gitlab
end.compact.uniq
end
+ def deny_all_requests_except_allowed?(should_deny)
+ should_deny.is_a?(Proc) ? should_deny.call : should_deny
+ end
+
+ def deny_all_requests_except_allowed_app_setting
+ Gitlab::CurrentSettings.current_application_settings? &&
+ Gitlab::CurrentSettings.deny_all_requests_except_allowed?
+ end
+
def object_storage_endpoint?(uri)
enabled_object_storage_endpoints.any? do |endpoint|
endpoint_uri = URI(endpoint)
@@ -312,17 +398,21 @@ module Gitlab
end
end
- def domain_allowed?(uri)
+ def domain_in_allow_list?(uri)
Gitlab::UrlBlockers::UrlAllowlist.domain_allowed?(uri.normalized_host, port: get_port(uri))
end
- def ip_allowed?(ip_address, port: nil)
+ def ip_in_allow_list?(ip_address, port: nil)
Gitlab::UrlBlockers::UrlAllowlist.ip_allowed?(ip_address, port: port)
end
def config
Gitlab.config
end
+
+ def no_proxy_env
+ ENV['no_proxy'] || ENV['NO_PROXY']
+ end
end
end
end