From 2cfcc3cdf6321b8082f2b54c5fd231633b3e3320 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 08:07:28 -0500 Subject: [PATCH 1/5] Fix MissingTemplate error when bots request non-HTML formats on home page Bots/crawlers requesting text/plain or other non-HTML formats caused a 500 error because no text template exists. Wrapping in respond_to returns 406 Not Acceptable for unsupported formats instead. Co-Authored-By: Claude Opus 4.6 --- app/controllers/home_controller.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index e5b73c49a..57e0c3ff2 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,6 +3,9 @@ class HomeController < ApplicationController def index authorize! :home - render :index + + respond_to do |format| + format.html { render :index } + end end end From f0ffb1283bf7dcf3145e475e25e5d9dbd50ddcf8 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 08:32:34 -0500 Subject: [PATCH 2/5] Add bot protection: global MissingTemplate rescue, robots.txt, rack-attack - Rescue ActionView::MissingTemplate globally in ApplicationController to return 406 instead of 500 when bots request non-HTML formats - Revert per-controller respond_to in HomeController (global rescue covers it) - Add allowlist-based robots.txt that only permits crawling public pages - Add rack-attack gem with login throttle and global rate limit Co-Authored-By: Claude Opus 4.6 --- Gemfile | 1 + app/controllers/application_controller.rb | 7 +++++++ app/controllers/home_controller.rb | 5 +---- config/initializers/rack_attack.rb | 16 ++++++++++++++++ public/robots.txt | 13 ++++++++++++- 5 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 config/initializers/rack_attack.rb diff --git a/Gemfile b/Gemfile index 10bba5ec1..08325f3b7 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem "jwt", "~> 1.2.1" gem "httparty" gem "will_paginate", "~> 3.1.7" gem "apipie-rails", "~> 1.5.0" +gem "rack-attack" gem "rack-cors", require: "rack/cors" # gem "ckeditor", "~> 4.3.0" # removed given gh security scan results. still need a replacement. gem "image_processing" diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3c5b9f000..6a9b0ce82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -17,6 +17,13 @@ class ApplicationController < ActionController::Base redirect_to root_path end + # Bots/crawlers requesting non-HTML formats (e.g. text/plain) trigger + # MissingTemplate errors because we only have .html.erb templates. + # Return 406 Not Acceptable instead of a 500 error. + rescue_from ActionView::MissingTemplate do + head :not_acceptable + end + private def after_sign_out_path_for(resource_or_scope) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 57e0c3ff2..e5b73c49a 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,9 +3,6 @@ class HomeController < ApplicationController def index authorize! :home - - respond_to do |format| - format.html { render :index } - end + render :index end end diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..3fd6f9ddc --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,16 @@ +Rack::Attack.cache.store = Rails.cache + +# Allow health check endpoint without throttling +Rack::Attack.safelist("allow-health-check") do |request| + request.path == "/up" +end + +# Throttle login attempts to 5 per 20 seconds per IP +Rack::Attack.throttle("logins/ip", limit: 5, period: 20.seconds) do |request| + request.ip if request.path == "/users/sign_in" && request.post? +end + +# Throttle all requests to 300 per 5 minutes per IP +Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |request| + request.ip +end diff --git a/public/robots.txt b/public/robots.txt index c19f78ab6..7c2a9cbe5 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1 +1,12 @@ -# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +User-agent: * +Allow: /workshops +Allow: /resources +Allow: /stories +Allow: /community_news +Allow: /events +Allow: /tutorials +Allow: /faqs +Allow: /contact_us +Allow: /tags +Allow: /story_shares +Disallow: / From f3c6b8ae05809b831f292da2439766c933c27406 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 08:42:41 -0500 Subject: [PATCH 3/5] Add rack-attack to Gemfile.lock Co-Authored-By: Claude Opus 4.6 --- Gemfile.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 62e61ce31..0d68128c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -567,6 +567,8 @@ GEM raabro (1.4.0) racc (1.8.1) rack (3.2.5) + rack-attack (6.8.0) + rack (>= 1.0, < 4) rack-cors (3.0.0) logger rack (>= 3.0.14) @@ -828,6 +830,7 @@ DEPENDENCIES pry-coolline pry-rails puma (~> 6.0) + rack-attack rack-cors rack-mini-profiler (~> 4.0) rails (~> 8.1.0) @@ -1062,6 +1065,7 @@ CHECKSUMS raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3 + rack-attack (6.8.0) sha256=f2499fdebf85bcc05573a22dff57d24305ac14ec2e4156cd3c28d47cafeeecf2 rack-cors (3.0.0) sha256=7b95be61db39606906b61b83bd7203fa802b0ceaaad8fcb2fef39e097bf53f68 rack-mini-profiler (4.0.1) sha256=485810c23211f908196c896ea10cad72ed68780ee2998bec1f1dfd7558263d78 rack-proxy (0.7.7) sha256=446a4b57001022145d5c3ba73b775f66a2260eaf7420c6907483141900395c8a From b1b13e73243569c20cab33e01515fa28ce8b953c Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 09:42:13 -0500 Subject: [PATCH 4/5] Replace rack-attack with Rails 8 built-in rate limiting Rails 8.1 provides native rate_limit in controllers, so no need for an external gem. Added global rate limit (300 req / 5 min per IP) in ApplicationController. Devise already handles login-specific protection via account locking after 10 failed attempts. Co-Authored-By: Claude Opus 4.6 --- Gemfile | 1 - Gemfile.lock | 4 ---- app/controllers/application_controller.rb | 2 ++ config/initializers/rack_attack.rb | 16 ---------------- 4 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 config/initializers/rack_attack.rb diff --git a/Gemfile b/Gemfile index 08325f3b7..10bba5ec1 100644 --- a/Gemfile +++ b/Gemfile @@ -30,7 +30,6 @@ gem "jwt", "~> 1.2.1" gem "httparty" gem "will_paginate", "~> 3.1.7" gem "apipie-rails", "~> 1.5.0" -gem "rack-attack" gem "rack-cors", require: "rack/cors" # gem "ckeditor", "~> 4.3.0" # removed given gh security scan results. still need a replacement. gem "image_processing" diff --git a/Gemfile.lock b/Gemfile.lock index 0d68128c6..62e61ce31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -567,8 +567,6 @@ GEM raabro (1.4.0) racc (1.8.1) rack (3.2.5) - rack-attack (6.8.0) - rack (>= 1.0, < 4) rack-cors (3.0.0) logger rack (>= 3.0.14) @@ -830,7 +828,6 @@ DEPENDENCIES pry-coolline pry-rails puma (~> 6.0) - rack-attack rack-cors rack-mini-profiler (~> 4.0) rails (~> 8.1.0) @@ -1065,7 +1062,6 @@ CHECKSUMS raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882 racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f rack (3.2.5) sha256=4cbd0974c0b79f7a139b4812004a62e4c60b145cba76422e288ee670601ed6d3 - rack-attack (6.8.0) sha256=f2499fdebf85bcc05573a22dff57d24305ac14ec2e4156cd3c28d47cafeeecf2 rack-cors (3.0.0) sha256=7b95be61db39606906b61b83bd7203fa802b0ceaaad8fcb2fef39e097bf53f68 rack-mini-profiler (4.0.1) sha256=485810c23211f908196c896ea10cad72ed68780ee2998bec1f1dfd7558263d78 rack-proxy (0.7.7) sha256=446a4b57001022145d5c3ba73b775f66a2260eaf7420c6907483141900395c8a diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6a9b0ce82..74a2b2230 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,6 @@ class ApplicationController < ActionController::Base + rate_limit to: 300, within: 5.minutes + before_action :authenticate_user! # ensures only logged-in users can access pages before_action :set_current_user # for AhoyTrackable in models before_action :preload_current_user_associations diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb deleted file mode 100644 index 3fd6f9ddc..000000000 --- a/config/initializers/rack_attack.rb +++ /dev/null @@ -1,16 +0,0 @@ -Rack::Attack.cache.store = Rails.cache - -# Allow health check endpoint without throttling -Rack::Attack.safelist("allow-health-check") do |request| - request.path == "/up" -end - -# Throttle login attempts to 5 per 20 seconds per IP -Rack::Attack.throttle("logins/ip", limit: 5, period: 20.seconds) do |request| - request.ip if request.path == "/users/sign_in" && request.post? -end - -# Throttle all requests to 300 per 5 minutes per IP -Rack::Attack.throttle("req/ip", limit: 300, period: 5.minutes) do |request| - request.ip -end From c953b4f2ffd3eba9706b5813dadb6950648c1962 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 09:45:26 -0500 Subject: [PATCH 5/5] Remove global rate limit to avoid false positives A blanket rate limit risks blocking legitimate users (e.g. search fields firing per-keystroke). Devise account locking already handles login brute-force. Targeted rate limits can be added later where needed. Co-Authored-By: Claude Opus 4.6 --- app/controllers/application_controller.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 74a2b2230..6a9b0ce82 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,4 @@ class ApplicationController < ActionController::Base - rate_limit to: 300, within: 5.minutes - before_action :authenticate_user! # ensures only logged-in users can access pages before_action :set_current_user # for AhoyTrackable in models before_action :preload_current_user_associations