Browse Source

Raise Mastodon::HostValidationError when host for HTTP request is private (#6410)

Akihiko Odaki 1 year ago
parent
commit
2e8a492e88

+ 4 - 0
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'

+ 2 - 0
Gemfile.lock

@@ -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)

+ 1 - 0
app/lib/exceptions.rb

@@ -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

+ 18 - 1
app/lib/request.rb

@@ -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 - 0
app/lib/sidekiq_error_handler.rb

@@ -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

+ 6 - 0
config/environments/development.rb

@@ -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

+ 4 - 0
config/initializers/sidekiq.rb

@@ -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|

+ 23 - 8
spec/lib/request_spec.rb

@@ -38,17 +38,32 @@ describe Request do
   end
 
   describe '#perform' do
-    before do
-      stub_request(:get, 'http://example.com')
-      subject.perform
-    end
+    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 'executes a HTTP request' do
-      expect(a_request(:get, 'http://example.com')).to have_been_made.once
+      it 'sets headers' do
+        expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
+      end
     end
 
-    it 'sets headers' do
-      expect(a_request(:get, 'http://example.com').with(headers: subject.headers)).to have_been_made
+    context 'with private host' do
+      around do |example|
+        WebMock.disable!
+        example.run
+        WebMock.enable!
+      end
+
+      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