From 55fe5a40a8ee65c08a6b41638bfe563a1e8a17a6 Mon Sep 17 00:00:00 2001 From: Molly Struve Date: Mon, 30 Mar 2020 11:09:40 -0500 Subject: [PATCH] Add Rack::Attack Gem to Throttle Requests, Only Search to start (#6841) [deploy] * throttle requests made to search endpoints * stub elasticsearch requests in rack attack specs and disable for running specs unless tagged * use original client IP from fastly to throttle requests to search endpoints --- Gemfile | 1 + Gemfile.lock | 3 +++ config/initializers/rack/attack.rb | 7 +++++++ spec/initializers/rack/attack_spec.rb | 26 ++++++++++++++++++++++++++ spec/rails_helper.rb | 8 ++++++++ 5 files changed, 45 insertions(+) create mode 100644 config/initializers/rack/attack.rb create mode 100644 spec/initializers/rack/attack_spec.rb diff --git a/Gemfile b/Gemfile index ece2e6f5d..edb0ffc27 100644 --- a/Gemfile +++ b/Gemfile @@ -72,6 +72,7 @@ gem "puma", "~> 4.3" # Puma is a simple, fast, threaded, and highly concurrent H gem "pundit", "~> 2.1" # Object oriented authorization for Rails applications gem "pusher", "~> 1.3" # Ruby library for Pusher Channels HTTP API gem "pusher-push-notifications", "~> 1.1" # Pusher Push Notifications Ruby server SDK +gem "rack-attack", "~> 6.2.2" # Used to throttle requests to prevent brute force attacks gem "rack-cors", "~> 1.1" # Middleware that will make Rack-based apps CORS compatible gem "rack-timeout", "~> 0.6" # Rack middleware which aborts requests that have been running for longer than a specified timeout gem "rails", "~> 5.2" # Ruby on Rails diff --git a/Gemfile.lock b/Gemfile.lock index d6aa93a42..eacfafbdb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -568,6 +568,8 @@ GEM rest-client (~> 2.0, >= 2.0.2) pusher-signature (0.1.8) rack (2.2.2) + rack-attack (6.2.2) + rack (>= 1.0, < 3) rack-cors (1.1.1) rack (>= 2.0.0) rack-host-redirect (1.3.0) @@ -958,6 +960,7 @@ DEPENDENCIES pundit-matchers (~> 1.6) pusher (~> 1.3) pusher-push-notifications (~> 1.1) + rack-attack (~> 6.2.2) rack-cors (~> 1.1) rack-host-redirect (~> 1.3) rack-timeout (~> 0.6) diff --git a/config/initializers/rack/attack.rb b/config/initializers/rack/attack.rb new file mode 100644 index 000000000..5829addf8 --- /dev/null +++ b/config/initializers/rack/attack.rb @@ -0,0 +1,7 @@ +class Rack::Attack + throttle("search_throttle", limit: 5, period: 1) do |request| + if request.path.starts_with?("/search/") && request.env["HTTP_FASTLY_CLIENT_IP"].present? + request.env["HTTP_FASTLY_CLIENT_IP"].to_s + end + end +end diff --git a/spec/initializers/rack/attack_spec.rb b/spec/initializers/rack/attack_spec.rb new file mode 100644 index 000000000..33fb6a952 --- /dev/null +++ b/spec/initializers/rack/attack_spec.rb @@ -0,0 +1,26 @@ +require "rails_helper" + +describe Rack::Attack, type: :request, throttle: true do + describe "search_throttle" do + before do + redis_url = "redis://localhost:6379" + cache_db = ActiveSupport::Cache::RedisStore.new(redis_url) + allow(Rails).to receive(:cache) { cache_db } + end + + it "throttles /search endpoints based on IP" do + Timecop.freeze do + allow(Search::User).to receive(:search_documents).and_return({}) + valid_responses = Array.new(5).map do + get "/search/users", headers: { "HTTP_FASTLY_CLIENT_IP" => "5.6.7.8" } + end + throttled_response = get "/search/users", headers: { "HTTP_FASTLY_CLIENT_IP" => "5.6.7.8" } + new_ip_response = get "/search/users", headers: { "HTTP_FASTLY_CLIENT_IP" => "1.1.1.1" } + + valid_responses.each { |r| expect(r).not_to eq(429) } + expect(throttled_response).to eq(429) + expect(new_ip_response).not_to eq(429) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 14b7ccd69..a2f6c82ff 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -53,6 +53,8 @@ WebMock.disable_net_connect!(allow_localhost: true, allow: allowed_sites) RSpec::Matchers.define_negated_matcher :not_change, :change +Rack::Attack.enabled = false + RSpec.configure do |config| config.use_transactional_fixtures = true config.fixture_path = "#{::Rails.root}/spec/fixtures" @@ -77,6 +79,12 @@ RSpec.configure do |config| example.run end + config.around(:each, throttle: true) do |example| + Rack::Attack.enabled = true + example.run + Rack::Attack.enabled = false + end + config.after do SiteConfig.clear_cache end