Raise Mastodon::HostValidationError when host for HTTP request is private (#6410)
This commit is contained in:
		
							parent
							
								
									7cb49eaa3a
								
							
						
					
					
						commit
						2e8a492e88
					
				
					 8 changed files with 69 additions and 9 deletions
				
			
		
							
								
								
									
										4
									
								
								Gemfile
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								Gemfile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -96,6 +96,10 @@ group :development, :test do
 | 
			
		|||
  gem 'rspec-rails', '~> 3.7'
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
group :production, :test do
 | 
			
		||||
  gem 'private_address_check', '~> 0.4.1'
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
group :test do
 | 
			
		||||
  gem 'capybara', '~> 2.15'
 | 
			
		||||
  gem 'climate_control', '~> 0.2'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -376,6 +376,7 @@ GEM
 | 
			
		|||
    premailer-rails (1.10.1)
 | 
			
		||||
      actionmailer (>= 3, < 6)
 | 
			
		||||
      premailer (~> 1.7, >= 1.7.9)
 | 
			
		||||
    private_address_check (0.4.1)
 | 
			
		||||
    pry (0.11.3)
 | 
			
		||||
      coderay (~> 1.1.0)
 | 
			
		||||
      method_source (~> 0.9.0)
 | 
			
		||||
| 
						 | 
				
			
			@ -683,6 +684,7 @@ DEPENDENCIES
 | 
			
		|||
  pghero (~> 1.7)
 | 
			
		||||
  pkg-config (~> 1.2)
 | 
			
		||||
  premailer-rails
 | 
			
		||||
  private_address_check (~> 0.4.1)
 | 
			
		||||
  pry-rails (~> 0.3)
 | 
			
		||||
  puma (~> 3.10)
 | 
			
		||||
  pundit (~> 1.1)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@ module Mastodon
 | 
			
		|||
  class Error < StandardError; end
 | 
			
		||||
  class NotPermittedError < Error; end
 | 
			
		||||
  class ValidationError < Error; end
 | 
			
		||||
  class HostValidationError < ValidationError; end
 | 
			
		||||
  class RaceConditionError < Error; end
 | 
			
		||||
 | 
			
		||||
  class UnexpectedResponseError < Error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,8 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'ipaddr'
 | 
			
		||||
require 'socket'
 | 
			
		||||
 | 
			
		||||
class Request
 | 
			
		||||
  REQUEST_TARGET = '(request-target)'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +11,7 @@ class Request
 | 
			
		|||
  def initialize(verb, url, **options)
 | 
			
		||||
    @verb    = verb
 | 
			
		||||
    @url     = Addressable::URI.parse(url).normalize
 | 
			
		||||
    @options = options
 | 
			
		||||
    @options = options.merge(socket_class: Socket)
 | 
			
		||||
    @headers = {}
 | 
			
		||||
 | 
			
		||||
    set_common_headers!
 | 
			
		||||
| 
						 | 
				
			
			@ -87,4 +90,18 @@ class Request
 | 
			
		|||
  def http_client
 | 
			
		||||
    HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class Socket < TCPSocket
 | 
			
		||||
    class << self
 | 
			
		||||
      def open(host, *args)
 | 
			
		||||
        address = IPSocket.getaddress(host)
 | 
			
		||||
        raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address)
 | 
			
		||||
        super address, *args
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      alias new open
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private_constant :Socket
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										11
									
								
								app/lib/sidekiq_error_handler.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/lib/sidekiq_error_handler.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class SidekiqErrorHandler
 | 
			
		||||
  def call(*)
 | 
			
		||||
    yield
 | 
			
		||||
  rescue Mastodon::HostValidationError => e
 | 
			
		||||
    Rails.logger.error "#{e.class}: #{e.message}"
 | 
			
		||||
    Rails.logger.error e.backtrace.join("\n")
 | 
			
		||||
    # Do not retry
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -85,3 +85,9 @@ Rails.application.configure do
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
ActiveRecordQueryTrace.enabled = ENV.fetch('QUERY_TRACE_ENABLED') { false }
 | 
			
		||||
 | 
			
		||||
module PrivateAddressCheck
 | 
			
		||||
  def self.private_address?(*)
 | 
			
		||||
    false
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,10 @@ end
 | 
			
		|||
 | 
			
		||||
Sidekiq.configure_server do |config|
 | 
			
		||||
  config.redis = redis_params
 | 
			
		||||
 | 
			
		||||
  config.server_middleware do |chain|
 | 
			
		||||
    chain.add SidekiqErrorHandler
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
Sidekiq.configure_client do |config|
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,17 +38,32 @@ describe Request do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#perform' do
 | 
			
		||||
    before do
 | 
			
		||||
      stub_request(:get, 'http://example.com')
 | 
			
		||||
      subject.perform
 | 
			
		||||
    context 'with valid host' do
 | 
			
		||||
      before do
 | 
			
		||||
        stub_request(:get, 'http://example.com')
 | 
			
		||||
        subject.perform
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'executes a HTTP request' do
 | 
			
		||||
        expect(a_request(:get, 'http://example.com')).to have_been_made.once
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it 'sets headers' do
 | 
			
		||||
        expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'executes a HTTP request' do
 | 
			
		||||
      expect(a_request(:get, 'http://example.com')).to have_been_made.once
 | 
			
		||||
    end
 | 
			
		||||
    context 'with private host' do
 | 
			
		||||
      around do |example|
 | 
			
		||||
        WebMock.disable!
 | 
			
		||||
        example.run
 | 
			
		||||
        WebMock.enable!
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    it 'sets headers' do
 | 
			
		||||
      expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
 | 
			
		||||
      it 'raises Mastodon::ValidationError' do
 | 
			
		||||
        allow(IPSocket).to receive(:getaddress).with('example.com').and_return('0.0.0.0')
 | 
			
		||||
        expect{ subject.perform }.to raise_error Mastodon::ValidationError
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue