From d8fd6bfcc298770da4334ccf0107a9ca439adfdb Mon Sep 17 00:00:00 2001 From: dadachi Date: Wed, 29 Apr 2026 18:37:35 +0900 Subject: [PATCH 1/2] Rate-limit shopkeeper sign-up to 5/IP/hour Mass account creation is currently only constrained by the broad 300/IP/5min rack-attack req/ip cap. Add an endpoint-specific limit on POST /shopkeeper_auth using Rails 8's built-in ActionController::RateLimiting: 5 requests per IP per hour. When exceeded, render 429 with a localized JSON error. Switch the test cache from :null_store to :memory_store so the limiter's counters can persist within a request sequence (:null_store no-ops increments). Clear Rails.cache in the standard test setup so throttle state doesn't leak between tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../registrations_controller.rb | 6 ++++ config/environments/test.rb | 4 ++- config/locales/en.yml | 1 + test/integration/sign_up_throttle_test.rb | 29 +++++++++++++++++++ test/test_helper.rb | 1 + 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 test/integration/sign_up_throttle_test.rb diff --git a/app/controllers/shopkeeper_auth/registrations_controller.rb b/app/controllers/shopkeeper_auth/registrations_controller.rb index b25469c..7a20d08 100644 --- a/app/controllers/shopkeeper_auth/registrations_controller.rb +++ b/app/controllers/shopkeeper_auth/registrations_controller.rb @@ -1,4 +1,10 @@ class ShopkeeperAuth::RegistrationsController < DeviseTokenAuth::RegistrationsController + rate_limit to: 5, within: 1.hour, only: :create, + with: -> { + render json: {code: 429, error_message: I18n.t("errors.messages.too_many_signups")}, + status: :too_many_requests + } + before_action :set_confirm_success_url, only: %i[create] before_action :configure_permitted_parameters diff --git a/config/environments/test.rb b/config/environments/test.rb index 411227e..30e57b4 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -22,7 +22,9 @@ # Show full error reports and disable caching. config.consider_all_requests_local = true - config.cache_store = :null_store + # Use memory store so ActionController::RateLimiting can persist counters + # between requests within a test; null_store would no-op the increments. + config.cache_store = :memory_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable diff --git a/config/locales/en.yml b/config/locales/en.yml index 626b6d1..c4dcd6e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -142,3 +142,4 @@ en: limit_count_shop: "You can create up to %{limit_count} shops across all organizations." limit_count_accounts_shopkeeper: "Organization members can be created up to %{limit_count}. Please contact the organization admin user or owner." limit_count_item_tag: "You can create up to %{limit_count} item tags." + too_many_signups: "Too many sign-up attempts. Please try again later." diff --git a/test/integration/sign_up_throttle_test.rb b/test/integration/sign_up_throttle_test.rb new file mode 100644 index 0000000..712f873 --- /dev/null +++ b/test/integration/sign_up_throttle_test.rb @@ -0,0 +1,29 @@ +require "test_helper" + +class SignUpThrottleTest < ActionDispatch::IntegrationTest + def post_sign_up(email) + post shopkeeper_registration_url, + params: { + name: "Throttle Test", + email: email, + password: "password", + password_confirmation: "password", + time_zone: "Tokyo", + current_platform: "ios" + }, + as: :json + end + + test "the sixth sign-up from the same IP within an hour is rate-limited" do + 5.times do |i| + post_sign_up("throttle#{i}@example.com") + assert_not_equal 429, response.status, "request #{i + 1} should not be throttled" + end + + post_sign_up("throttle5@example.com") + + assert_response :too_many_requests + assert_equal 429, response.parsed_body["code"] + assert_equal I18n.t("errors.messages.too_many_signups"), response.parsed_body["error_message"] + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 59401c1..d44ecea 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,6 +14,7 @@ module ActiveSupport class TestCase setup do Dir[Rails.root.join("db", "fixtures", "test", "*.rb")].sort.each { |s| load s } + Rails.cache.clear end # Run tests in parallel with specified workers From 75632fd92f4224d441553a7a0b96fbc0aaa17f3a Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 30 Apr 2026 06:40:04 +0900 Subject: [PATCH 2/2] Tune sign-up rate limit to 10/3min (match jumpstart-pro reference) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with the reference codebase (jumpstart-pro-rails uses `to: 10, within: 3.minutes` on user sign-up). The earlier 5/1.hour was overly strict for legit users — a confused user with bad password rules or autofill misfires could exhaust the quota and get locked out for an hour. 10/3min absorbs realistic retry flows while still constraining bots; per-IP limits are not the primary defense against rotating-IP attackers anyway. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/controllers/shopkeeper_auth/registrations_controller.rb | 2 +- test/integration/sign_up_throttle_test.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/shopkeeper_auth/registrations_controller.rb b/app/controllers/shopkeeper_auth/registrations_controller.rb index 7a20d08..fd5d44e 100644 --- a/app/controllers/shopkeeper_auth/registrations_controller.rb +++ b/app/controllers/shopkeeper_auth/registrations_controller.rb @@ -1,5 +1,5 @@ class ShopkeeperAuth::RegistrationsController < DeviseTokenAuth::RegistrationsController - rate_limit to: 5, within: 1.hour, only: :create, + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render json: {code: 429, error_message: I18n.t("errors.messages.too_many_signups")}, status: :too_many_requests diff --git a/test/integration/sign_up_throttle_test.rb b/test/integration/sign_up_throttle_test.rb index 712f873..f440de1 100644 --- a/test/integration/sign_up_throttle_test.rb +++ b/test/integration/sign_up_throttle_test.rb @@ -14,13 +14,13 @@ def post_sign_up(email) as: :json end - test "the sixth sign-up from the same IP within an hour is rate-limited" do - 5.times do |i| + test "the eleventh sign-up from the same IP within the window is rate-limited" do + 10.times do |i| post_sign_up("throttle#{i}@example.com") assert_not_equal 429, response.status, "request #{i + 1} should not be throttled" end - post_sign_up("throttle5@example.com") + post_sign_up("throttle10@example.com") assert_response :too_many_requests assert_equal 429, response.parsed_body["code"]