From 1893bd4e85236ea92bc39de2eb06f1c7cfb54db0 Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Wed, 30 Jul 2025 15:37:02 +0200 Subject: [PATCH 1/2] feat: update rack attack config --- config/initializers/rack_attack.rb | 12 +++++++++++- lib/decidim_app/rack_attack.rb | 24 +++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 62de9c45..4fd22327 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -6,4 +6,14 @@ # Enabled by default in production # Can be deactivated with 'ENABLE_RACK_ATTACK=0' -DecidimApp::RackAttack.apply_configuration if DecidimApp::RackAttack.rack_enabled? + +DecidimApp::RackAttack.deactivate_decidim_throttling! + +if DecidimApp::RackAttack.rack_enabled? + DecidimApp::RackAttack.enable_rack_attack! + DecidimApp::RackAttack.apply_configuration +else + DecidimApp::RackAttack.disable_rack_attack! +end + +DecidimApp::RackAttack.info! diff --git a/lib/decidim_app/rack_attack.rb b/lib/decidim_app/rack_attack.rb index da81cd82..00ebd86a 100644 --- a/lib/decidim_app/rack_attack.rb +++ b/lib/decidim_app/rack_attack.rb @@ -4,21 +4,39 @@ module DecidimApp module RackAttack def self.rack_enabled? setting = Rails.application.secrets.dig(:decidim, :rack_attack, :enabled) - return setting == "1" if setting.present? + return setting.to_s == "1" if setting.present? Rails.env.production? end - def self.apply_configuration + def self.info! + Rails.logger.info("Rack::Attack is enabled: #{Rack::Attack.enabled}") + Rails.logger.info("Rack::Attack Fail2ban is enabled: #{DecidimApp::RackAttack::Fail2ban.enabled?}") + Rack::Attack.throttles.keys.each do |throttle| + Rails.logger.info("Rack::Attack throttling registered: #{throttle}") + end + end + + def self.enable_rack_attack! + Rails.logger.info("Rack::Attack is now enabled") Rack::Attack.enabled = true + end + def self.disable_rack_attack! + Rails.logger.info("Rack::Attack is now disabled") + Rack::Attack.enabled = false + end + + def self.deactivate_decidim_throttling! # Remove the original throttle from decidim-core - # see https://github.com/decidim/decidim/blob/release/0.26-stable/decidim-core/config/initializers/rack_attack.rb#L19 + # see https://github.com/decidim/decidim/blob/release/0.27-stable/decidim-core/config/initializers/rack_attack.rb#L19 DecidimApp::RackAttack::Throttling.deactivate_decidim_throttling! do Rails.logger.info("Deactivating 'requests by ip' from Decidim Core") Rack::Attack.throttles.delete("requests by ip") end + end + def self.apply_configuration Rack::Attack.throttled_response_retry_after_header = true Rack::Attack.throttled_responder = lambda do |request| From 18b2308645ea5b85be5d072987dc3a73f3b6992d Mon Sep 17 00:00:00 2001 From: stephanie rousset Date: Wed, 30 Jul 2025 15:37:22 +0200 Subject: [PATCH 2/2] test: update rack attack test --- spec/lib/decidim_app/rack_attack_spec.rb | 82 ++++++++++++++++++------ 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/spec/lib/decidim_app/rack_attack_spec.rb b/spec/lib/decidim_app/rack_attack_spec.rb index 0aca1063..83b8d4be 100644 --- a/spec/lib/decidim_app/rack_attack_spec.rb +++ b/spec/lib/decidim_app/rack_attack_spec.rb @@ -62,40 +62,76 @@ end end - describe "#apply_configuration" do + describe "#enable_rack_attack!" do + before do + described_class.enable_rack_attack! + end + + it "enables Rack::Attack" do + expect(Rack::Attack.enabled).to be_truthy + end + end + + describe "#disable_rack_attack!" do before do - described_class.apply_configuration - Rack::Attack.reset! + described_class.disable_rack_attack! end + it "enables Rack::Attack" do + expect(Rack::Attack.enabled).to be_falsey + end + end + + describe "#deactivate_decidim_throttling!" do + before do + # Decidim throttling is deactivated by default in rails env test + # https://github.com/decidim/decidim/blob/release/0.27-stable/decidim-core/config/initializers/rack_attack.rb#L19 + # so we add some keys to test the method + Rack::Attack.throttle("limit confirmations attempts per code", limit: 5, period: 60.seconds) # added by module friendly_signup in decidim_app + Rack::Attack.throttle("requests by ip", limit: 5, period: 60.seconds) + described_class.deactivate_decidim_throttling! + end + + after do + # delete the added keys + Rack::Attack.throttles.delete("requests by ip") + Rack::Attack.throttles.delete("limit confirmations attempts per code") + end + + it "deactivates Decidim throttling" do + expect(Rack::Attack.throttles.keys.join).to include("limit confirmations attempts per code") + expect(Rack::Attack.throttles.keys.join).not_to include("requests by ip") + end + end + + describe "#apply_configuration" do describe "Throttling" do let(:headers) { { "REMOTE_ADDR" => "1.2.3.4", "decidim.current_organization" => organization } } + let(:rack_max_requests) { 15 } - it "successful for 100 requests, then blocks the user" do - 100.times do - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:ok) - end - - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:too_many_requests) - expect(response.body).to include("Your connection has been slowed because server received too many requests.") + before do + allow(Rails.application.secrets).to receive(:dig).with(any_args).and_call_original + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :rack_attack, :throttle, :max_requests).and_return(rack_max_requests) + described_class.apply_configuration + Rack::Attack.reset! + described_class.enable_rack_attack! + end - travel_to(1.minute.from_now) do - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:ok) - end + it "defines default period and max_requests" do + expect(DecidimApp::RackAttack::Throttling.max_requests).to eq(rack_max_requests) + expect(DecidimApp::RackAttack::Throttling.period).to eq(60) end - it "successful for 99 requests" do - 99.times do + it "successful for 15 requests, then blocks the user" do + rack_max_requests.times do get decidim.root_path, params: {}, headers: headers expect(response).to have_http_status(:ok) + expect(response.body).not_to include("Your connection has been slowed because server received too many requests.") end get decidim.root_path, params: {}, headers: headers - expect(response.body).not_to include("Your connection has been slowed because server received too many requests.") - expect(response).not_to have_http_status(:too_many_requests) + expect(response).to have_http_status(:too_many_requests) + expect(response.body).to include("Your connection has been slowed because server received too many requests.") travel_to(1.minute.from_now) do get decidim.root_path, params: {}, headers: headers @@ -107,6 +143,12 @@ describe "Fail2Ban" do let(:headers) { { "REMOTE_ADDR" => "1.2.3.4", "decidim.current_organization" => organization } } + before do + described_class.apply_configuration + Rack::Attack.reset! + described_class.enable_rack_attack! + end + %w(/etc/passwd /wp-admin/index.php /wp-login/index.php SELECT CONCAT /.git/config).each do |path| it "blocks user for specific request : '#{path}'" do get "#{decidim.root_path}#{path}", params: {}, headers: headers