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

rebase/4.0.0rc2
Akihiko Odaki 2018-02-25 03:16:11 +09:00 committed by Eugen Rochko
parent 7cb49eaa3a
commit 2e8a492e88
8 changed files with 69 additions and 9 deletions

View File

@ -96,6 +96,10 @@ group :development, :test do
gem 'rspec-rails', '~> 3.7' gem 'rspec-rails', '~> 3.7'
end end
group :production, :test do
gem 'private_address_check', '~> 0.4.1'
end
group :test do group :test do
gem 'capybara', '~> 2.15' gem 'capybara', '~> 2.15'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'

View File

@ -376,6 +376,7 @@ GEM
premailer-rails (1.10.1) premailer-rails (1.10.1)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.4.1)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
@ -683,6 +684,7 @@ DEPENDENCIES
pghero (~> 1.7) pghero (~> 1.7)
pkg-config (~> 1.2) pkg-config (~> 1.2)
premailer-rails premailer-rails
private_address_check (~> 0.4.1)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.10) puma (~> 3.10)
pundit (~> 1.1) pundit (~> 1.1)

View File

@ -4,6 +4,7 @@ module Mastodon
class Error < StandardError; end class Error < StandardError; end
class NotPermittedError < Error; end class NotPermittedError < Error; end
class ValidationError < Error; end class ValidationError < Error; end
class HostValidationError < ValidationError; end
class RaceConditionError < Error; end class RaceConditionError < Error; end
class UnexpectedResponseError < Error class UnexpectedResponseError < Error

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'ipaddr'
require 'socket'
class Request class Request
REQUEST_TARGET = '(request-target)' REQUEST_TARGET = '(request-target)'
@ -8,7 +11,7 @@ class Request
def initialize(verb, url, **options) def initialize(verb, url, **options)
@verb = verb @verb = verb
@url = Addressable::URI.parse(url).normalize @url = Addressable::URI.parse(url).normalize
@options = options @options = options.merge(socket_class: Socket)
@headers = {} @headers = {}
set_common_headers! set_common_headers!
@ -87,4 +90,18 @@ class Request
def http_client def http_client
HTTP.timeout(:per_operation, timeout).follow(max_hops: 2) HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
end 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 end

View 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

View File

@ -85,3 +85,9 @@ Rails.application.configure do
end end
ActiveRecordQueryTrace.enabled = ENV.fetch('QUERY_TRACE_ENABLED') { false } ActiveRecordQueryTrace.enabled = ENV.fetch('QUERY_TRACE_ENABLED') { false }
module PrivateAddressCheck
def self.private_address?(*)
false
end
end

View File

@ -9,6 +9,10 @@ end
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = redis_params config.redis = redis_params
config.server_middleware do |chain|
chain.add SidekiqErrorHandler
end
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|

View File

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