From 0bd26af2dd7a22c34b92169cc1391abcb44564de Mon Sep 17 00:00:00 2001 From: KMY Date: Sun, 2 Jun 2024 11:03:56 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=AA?= =?UTF-8?q?=E3=82=A2=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AB=E5=8E=B3?= =?UTF-8?q?=E3=81=97=E3=81=84=E3=83=AC=E3=83=BC=E3=83=88=E3=83=AA=E3=83=9F?= =?UTF-8?q?=E3=83=83=E3=83=88=E3=81=8C=E9=81=A9=E7=94=A8=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/initializers/rack_attack.rb | 4 +- spec/config/initializers/rack/attack_spec.rb | 83 ++++++++++++++++---- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 14fab7ecda..a3c2b821cf 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -141,8 +141,10 @@ class Rack::Attack req.session[:attempt_user_id] || req.params.dig('user', 'email').presence if req.post? && req.path_matches?('/auth/sign_in') end + API_CREATE_EMOJI_REACTION_REGEX = %r{\A/api/v1/statuses/\d+/emoji_reactions} + throttle('throttle_password_change/account', limit: 10, period: 10.minutes) do |req| - req.warden_user_id if req.put? || (req.patch? && req.path_matches?('/auth')) + req.warden_user_id if (req.put? && !req.path.match?(API_CREATE_EMOJI_REACTION_REGEX)) || (req.patch? && req.path_matches?('/auth')) end self.throttled_responder = lambda do |request| diff --git a/spec/config/initializers/rack/attack_spec.rb b/spec/config/initializers/rack/attack_spec.rb index 19de480898..59d0462695 100644 --- a/spec/config/initializers/rack/attack_spec.rb +++ b/spec/config/initializers/rack/attack_spec.rb @@ -7,7 +7,7 @@ describe Rack::Attack, type: :request do Rails.application end - shared_examples 'throttled endpoint' do + shared_context 'with throttled endpoint base' do before do # Rack::Attack periods are not rolling, so avoid flaky tests by setting the time in a way # to avoid crossing period boundaries. @@ -19,6 +19,30 @@ describe Rack::Attack, type: :request do travel_to Time.zone.at(counter_prefix * period.seconds) end + def below_limit + limit - 1 + end + + def above_limit + limit * 2 + end + + def throttle_count + described_class.cache.read("#{counter_prefix}:#{throttle}:#{discriminator}") || 0 + end + + def counter_prefix + (Time.now.to_i / period.seconds).to_i + end + + def increment_counter + described_class.cache.count("#{throttle}:#{discriminator}", period) + end + end + + shared_examples 'throttled endpoint' do + include_examples 'with throttled endpoint base' + context 'when the number of requests is lower than the limit' do before do below_limit.times { increment_counter } @@ -46,25 +70,32 @@ describe Rack::Attack, type: :request do expect(response).to_not have_http_status(429) end end + end - def below_limit - limit - 1 + shared_examples 'does not throttle endpoint' do + include_examples 'with throttled endpoint base' + + context 'when the number of requests is lower than the limit' do + before do + below_limit.times { increment_counter } + end + + it 'does not change the request status' do + expect { request.call }.to change { throttle_count }.by(0) + + expect(response).to_not have_http_status(429) + end end - def above_limit - limit * 2 - end + context 'when the number of requests is higher than the limit' do + before do + above_limit.times { increment_counter } + end - def throttle_count - described_class.cache.read("#{counter_prefix}:#{throttle}:#{discriminator}") || 0 - end - - def counter_prefix - (Time.now.to_i / period.seconds).to_i - end - - def increment_counter - described_class.cache.count("#{throttle}:#{discriminator}", period) + it 'returns http too many requests after limit and returns to normal status after period' do + expect { request.call }.to change { throttle_count }.by(0) + expect(response).to_not have_http_status(429) + end end end @@ -176,4 +207,24 @@ describe Rack::Attack, type: :request do it_behaves_like 'throttled endpoint' end + + describe 'throttle excessive emoji reaction requests by account' do + let(:user) { Fabricate(:user, email: 'user@host.example') } + let(:throttle) { 'throttle_password_change/account' } + let(:limit) { 10 } + let(:period) { 10.minutes } + let(:request) { -> { put path, headers: { 'REMOTE_ADDR' => remote_ip } } } + let(:status) { Fabricate(:status) } + let(:emoji) { Fabricate(:custom_emoji) } + let(:path) { "/api/v1/statuses/#{status.id}/emoji_reactions/#{emoji.shortcode}" } + let(:discriminator) { user.id } + + before do + sign_in user, scope: :user + + get '/' + end + + it_behaves_like 'does not throttle endpoint' + end end