diff --git a/Gemfile b/Gemfile index 020990ae5..17d37eee9 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,7 @@ gem 'prawn_rails' gem 'premailer-rails' gem 'puma' # Use Puma as the app server gem 'pundit' +gem 'rack-attack' gem 'rack-mini-profiler' gem 'rails' gem 'rails-i18n' # Locales par défaut diff --git a/Gemfile.lock b/Gemfile.lock index 055c310e9..0f27aff81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -434,6 +434,8 @@ GEM pundit (2.0.1) activesupport (>= 3.0.0) rack (2.0.6) + rack-attack (6.0.0) + rack (>= 1.0, < 3) rack-mini-profiler (1.0.1) rack (>= 1.2.0) rack-oauth2 (1.9.3) @@ -752,6 +754,7 @@ DEPENDENCIES pry-byebug puma pundit + rack-attack rack-mini-profiler rails rails-controller-testing diff --git a/app/services/ip_service.rb b/app/services/ip_service.rb index 6ca7fdc5e..2f69c55d2 100644 --- a/app/services/ip_service.rb +++ b/app/services/ip_service.rb @@ -3,13 +3,7 @@ class IPService def ip_trusted?(ip) ip_address = parse_address(ip) - if ip_address.nil? - false - elsif trusted_networks.present? - trusted_networks.any? { |network| network.include?(ip_address) } - else - false - end + trusted_networks.any? { |network| network.include?(ip_address) } end private diff --git a/config/application.rb b/config/application.rb index be464bbf2..5f6cbc055 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,5 +41,6 @@ module TPS end config.ds_weekly_overview = ENV['APP_NAME'] == 'tps' + config.middleware.use Rack::Attack end end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..9a18f8936 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,27 @@ +class Rack::Attack + throttle('/users/sign_in/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/users/sign_in' && req.post? && rack_attack_enabled? + req.remote_ip + end + end + + throttle('stats/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/stats' && rack_attack_enabled? + req.remote_ip + end + end + + throttle('contact/ip', limit: 5, period: 20.seconds) do |req| + if req.path == '/contact' && req.post? && rack_attack_enabled? + req.remote_ip + end + end + + Rack::Attack.safelist('allow from localhost') do |req| + IPService.ip_trusted?(req.remote_ip) + end + + def self.rack_attack_enabled? + ENV['RACK_ATTACK_ENABLE'] == 'true' + end +end diff --git a/config/initializers/rack_attack_request.rb b/config/initializers/rack_attack_request.rb new file mode 100644 index 000000000..fa72e9844 --- /dev/null +++ b/config/initializers/rack_attack_request.rb @@ -0,0 +1,7 @@ +class Rack::Attack + class Request < ::Rack::Request + def remote_ip + @remote_ip ||= (env['action_dispatch.remote_ip'] || ip).to_s + end + end +end diff --git a/spec/middlewares/rack_attack_spec.rb b/spec/middlewares/rack_attack_spec.rb new file mode 100644 index 000000000..1ec71cd96 --- /dev/null +++ b/spec/middlewares/rack_attack_spec.rb @@ -0,0 +1,56 @@ +require "rails_helper" + +describe Rack::Attack, type: :request do + let(:limit) { 5 } + let(:period) { 20 } + let(:ip) { "1.2.3.4" } + + before(:each) do + ENV['RACK_ATTACK_ENABLE'] = 'true' + setup_rack_attack_cache_store + avoid_test_overlaps_in_cache + end + + after do + ENV['RACK_ATTACK_ENABLE'] = 'false' + end + + def setup_rack_attack_cache_store + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + end + + def avoid_test_overlaps_in_cache + Rails.cache.clear + end + + context '/users/sign_in' do + before do + limit.times do + Rack::Attack.cache.count("/users/sign_in/ip:#{ip}", period) + end + end + + subject do + post "/users/sign_in", headers: { 'X-Forwarded-For': ip } + end + + it "throttle excessive requests by IP address" do + subject + + expect(response).to have_http_status(:too_many_requests) + end + + context 'when the ip is whitelisted' do + before do + allow(IPService).to receive(:ip_trusted?).and_return(true) + allow_any_instance_of(Users::SessionsController).to receive(:create).and_return(:ok) + end + + it "respects the whitelist" do + subject + + expect(response).not_to have_http_status(:too_many_requests) + end + end + end +end diff --git a/spec/services/ip_service_spec.rb b/spec/services/ip_service_spec.rb index c9c1d1641..2d2798fa0 100644 --- a/spec/services/ip_service_spec.rb +++ b/spec/services/ip_service_spec.rb @@ -28,6 +28,18 @@ describe IPService do it { is_expected.to be(false) } end + + context 'when the trusted network is not defined' do + it { is_expected.to be(false) } + end + + context 'when the trusted network is malformed' do + before do + ENV['TRUSTED_NETWORKS'] = 'bad network' + end + + it { is_expected.to be(false) } + end end context 'when a trusted network is defined' do