From db2b2d033b0b581c6c0b0ec15fe3c94649fcf0f3 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 22:44:55 -0700 Subject: [PATCH 1/8] Add honeypot_enabled config option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the existing `spinner_enabled` / `timestamp_enabled` switches. When `InvisibleCaptcha.honeypot_enabled = false`: - the view helper renders only the spinner field (no hidden text input for password managers to autofill into) - the controller-side `honeypot_spam?` check returns false without inspecting params Use case: in some apps (especially those whose users heavily lean on password managers like 1Password / Bitwarden / Dashlane), the manager autofills the honeypot text input with an unrelated saved value, the controller treats the submission as spam, and the gem responds with `head(200)` — a blank white page with no flash. Real users get blocked. Setting `honeypots = []` works as a workaround but still emits a stray nameless text input in the DOM (the gem's view helper always renders a honeypot field). This option lets the user disable the honeypot explicitly while keeping timestamp + spinner protection. Default is true (no behavior change for existing apps). --- CHANGELOG.md | 4 ++++ README.md | 2 ++ lib/invisible_captcha.rb | 6 ++++++ lib/invisible_captcha/controller_ext.rb | 2 +- lib/invisible_captcha/view_helpers.rb | 8 ++++++++ spec/controllers_spec.rb | 18 ++++++++++++++++++ spec/view_helpers_spec.rb | 25 +++++++++++++++++++++++++ 7 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f20616..e3b71e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +- New `honeypot_enabled` config option (default true). When set to false, the view helper renders no honeypot text input and the controller skips the honeypot param check, while leaving timestamp + spinner protection intact. Useful when password-manager autofill of the honeypot is producing false positives that outweigh the bot-blocking benefit. + ## [2.3.0] - Run honeypot + spinner checks and their callback also if timestamp triggers but passes through (#132) diff --git a/README.md b/README.md index 2b12cd7..928e099 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ You can customize: - `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default. - `injectable_styles`: if enabled, you should call anywhere in your layout the following helper `<%= invisible_captcha_styles %>`. This allows you to inject styles, for example, in ``. False by default, styles are injected inline with the honeypot. - `spinner_enabled`: option to disable the IP spinner validation. By default, true. +- `honeypot_enabled`: option to disable the honeypot field at the application level. When false, the view helper renders no hidden text input (only the spinner, if `spinner_enabled`) and the controller skips the honeypot param check. Useful if a password manager is autofilling the honeypot for some of your users — false positives outweigh the protection in some setups. By default, true. - `secret`: customize the secret key to encode some internal values. By default, it reads the environment variable `ENV['INVISIBLE_CAPTCHA_SECRET']` and fallbacks to random value. Be careful, if you are running multiple Rails instances behind a load balancer, use always the same value via the environment variable. To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`): @@ -130,6 +131,7 @@ InvisibleCaptcha.setup do |config| # config.timestamp_enabled = true # config.injectable_styles = false # config.spinner_enabled = true + # config.honeypot_enabled = true # Leave these unset if you want to use I18n (see below) # config.sentence_for_humans = 'If you are a human, ignore this field' diff --git a/lib/invisible_captcha.rb b/lib/invisible_captcha.rb index 6d2b324..07530fd 100644 --- a/lib/invisible_captcha.rb +++ b/lib/invisible_captcha.rb @@ -17,6 +17,7 @@ class << self :visual_honeypots, :injectable_styles, :spinner_enabled, + :honeypot_enabled, :secret def init! @@ -42,6 +43,11 @@ def init! # Spinner check enabled by default self.spinner_enabled = true + # Honeypot check enabled by default. When false, the view helper renders + # only the spinner field (no hidden text input for password managers to + # autofill) and the controller skips the honeypot param check. + self.honeypot_enabled = true + # A secret key to encode some internal values self.secret = ENV['INVISIBLE_CAPTCHA_SECRET'] || SecureRandom.hex(64) end diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index a816991..320a8e2 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -24,7 +24,7 @@ def detect_spam(options = {}) return if performed? end - if honeypot_spam?(options) || spinner_spam? + if (InvisibleCaptcha.honeypot_enabled && honeypot_spam?(options)) || spinner_spam? on_spam(options) end end diff --git a/lib/invisible_captcha/view_helpers.rb b/lib/invisible_captcha/view_helpers.rb index bbd7981..28d76e1 100644 --- a/lib/invisible_captcha/view_helpers.rb +++ b/lib/invisible_captcha/view_helpers.rb @@ -38,6 +38,8 @@ def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) honeypot = nil end + return build_spinner_only unless InvisibleCaptcha.honeypot_enabled + honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot label = options.delete(:sentence_for_humans) || InvisibleCaptcha.sentence_for_humans css_class = "#{honeypot}_#{Time.zone.now.to_i}" @@ -58,6 +60,12 @@ def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) end end + def build_spinner_only + return ''.html_safe unless InvisibleCaptcha.spinner_enabled + + hidden_field_tag('spinner', session[:invisible_captcha_spinner], id: nil) + end + def visibility_css(css_class, options) visible = if options.key?(:visual_honeypots) options.delete(:visual_honeypots) diff --git a/spec/controllers_spec.rb b/spec/controllers_spec.rb index dad7ecb..7649cfb 100644 --- a/spec/controllers_spec.rb +++ b/spec/controllers_spec.rb @@ -223,6 +223,24 @@ def custom_timestamp_callback end end + context 'with honeypot_enabled = false' do + before(:each) do + InvisibleCaptcha.honeypot_enabled = false + session[:invisible_captcha_timestamp] = Time.zone.now.iso8601 + + # Wait for valid submission + sleep InvisibleCaptcha.timestamp_threshold + end + + after(:each) { InvisibleCaptcha.honeypot_enabled = true } + + it 'passes even when the honeypot field is filled' do + post :create, params: { topic: { subtitle: 'spam-value', title: 'foo' } } + + expect(response.body).to be_present + end + end + context 'spinner attribute' do before(:each) do InvisibleCaptcha.spinner_enabled = true diff --git a/spec/view_helpers_spec.rb b/spec/view_helpers_spec.rb index 278bdb3..90f11ec 100644 --- a/spec/view_helpers_spec.rb +++ b/spec/view_helpers_spec.rb @@ -75,6 +75,31 @@ end end + context "should have honeypot field" do + it 'that exists by default, honeypot_enabled is true' do + InvisibleCaptcha.honeypot_enabled = true + InvisibleCaptcha.honeypots = [:foo_id] + expect(invisible_captcha).to match(/name="foo_id"/) + end + + it 'that does not exist if honeypot_enabled is false' do + InvisibleCaptcha.honeypot_enabled = false + expect(invisible_captcha).not_to match(/type="text"/) + end + + it 'still renders the spinner if honeypot_enabled is false and spinner_enabled is true' do + InvisibleCaptcha.honeypot_enabled = false + InvisibleCaptcha.spinner_enabled = true + expect(invisible_captcha).to match(/name="spinner"/) + end + + it 'renders nothing if both honeypot_enabled and spinner_enabled are false' do + InvisibleCaptcha.honeypot_enabled = false + InvisibleCaptcha.spinner_enabled = false + expect(invisible_captcha).to eq('') + end + end + it 'should set spam timestamp' do invisible_captcha expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601) From b15eed159cab9d94bdb3cc02f6a2987a89f8ff56 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 23:50:39 -0700 Subject: [PATCH 2/8] Refactor honeypot handling in view helpers --- lib/invisible_captcha/view_helpers.rb | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/invisible_captcha/view_helpers.rb b/lib/invisible_captcha/view_helpers.rb index 28d76e1..96a95b8 100644 --- a/lib/invisible_captcha/view_helpers.rb +++ b/lib/invisible_captcha/view_helpers.rb @@ -38,11 +38,11 @@ def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) honeypot = nil end - return build_spinner_only unless InvisibleCaptcha.honeypot_enabled - - honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot - label = options.delete(:sentence_for_humans) || InvisibleCaptcha.sentence_for_humans - css_class = "#{honeypot}_#{Time.zone.now.to_i}" + if InvisibleCaptcha.honeypot_enabled + honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot + label = options.delete(:sentence_for_humans) || InvisibleCaptcha.sentence_for_humans + css_class = "#{honeypot}_#{Time.zone.now.to_i}" + end styles = visibility_css(css_class, options) @@ -52,20 +52,16 @@ def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) content_tag(:div, class: css_class) do concat styles unless InvisibleCaptcha.injectable_styles - concat label_tag(build_label_name(honeypot, scope), label) - concat text_field_tag(build_input_name(honeypot, scope), nil, default_honeypot_options.merge(options)) + if InvisibleCaptcha.honeypot_enabled + concat label_tag(build_label_name(honeypot, scope), label) + concat text_field_tag(build_input_name(honeypot, scope), nil, default_honeypot_options.merge(options)) + end if InvisibleCaptcha.spinner_enabled concat hidden_field_tag("spinner", session[:invisible_captcha_spinner], id: nil) end end end - def build_spinner_only - return ''.html_safe unless InvisibleCaptcha.spinner_enabled - - hidden_field_tag('spinner', session[:invisible_captcha_spinner], id: nil) - end - def visibility_css(css_class, options) visible = if options.key?(:visual_honeypots) options.delete(:visual_honeypots) From a6a40ec0361ade9f8824534b5f4f9d08399dc866 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 23:54:37 -0700 Subject: [PATCH 3/8] refactor --- lib/invisible_captcha/controller_ext.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index 320a8e2..038b2b1 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -24,7 +24,7 @@ def detect_spam(options = {}) return if performed? end - if (InvisibleCaptcha.honeypot_enabled && honeypot_spam?(options)) || spinner_spam? + if honeypot_spam?(options) || spinner_spam? on_spam(options) end end @@ -85,6 +85,8 @@ def spinner_spam? end def honeypot_spam?(options = {}) + return false unless InvisibleCaptcha.honeypot_enabled + honeypot = options[:honeypot] scope = options[:scope] || controller_name.singularize From ffc3eac7cf40f8a3cb3154eca966d5f735017d76 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 23:55:45 -0700 Subject: [PATCH 4/8] comment --- lib/invisible_captcha.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/invisible_captcha.rb b/lib/invisible_captcha.rb index 07530fd..e447108 100644 --- a/lib/invisible_captcha.rb +++ b/lib/invisible_captcha.rb @@ -43,9 +43,7 @@ def init! # Spinner check enabled by default self.spinner_enabled = true - # Honeypot check enabled by default. When false, the view helper renders - # only the spinner field (no hidden text input for password managers to - # autofill) and the controller skips the honeypot param check. + # Honeypot check enabled by default self.honeypot_enabled = true # A secret key to encode some internal values From 190fa5d285bfb846d8a3c45ffcad5108daab0fa1 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 23:57:07 -0700 Subject: [PATCH 5/8] Update CHANGELOG with new honeypot_enabled option --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b71e7..e139fe4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] -- New `honeypot_enabled` config option (default true). When set to false, the view helper renders no honeypot text input and the controller skips the honeypot param check, while leaving timestamp + spinner protection intact. Useful when password-manager autofill of the honeypot is producing false positives that outweigh the bot-blocking benefit. +- New `honeypot_enabled` config option (default true). ## [2.3.0] From dd37902f1134810de19541cabca52598a0f4f407 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Tue, 9 Jun 2026 23:58:04 -0700 Subject: [PATCH 6/8] Update honeypot_enabled option description in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 928e099..d855424 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ You can customize: - `timestamp_error_message`: flash error message thrown when form submitted quicker than the `timestamp_threshold` value. It uses I18n by default. - `injectable_styles`: if enabled, you should call anywhere in your layout the following helper `<%= invisible_captcha_styles %>`. This allows you to inject styles, for example, in ``. False by default, styles are injected inline with the honeypot. - `spinner_enabled`: option to disable the IP spinner validation. By default, true. -- `honeypot_enabled`: option to disable the honeypot field at the application level. When false, the view helper renders no hidden text input (only the spinner, if `spinner_enabled`) and the controller skips the honeypot param check. Useful if a password manager is autofilling the honeypot for some of your users — false positives outweigh the protection in some setups. By default, true. +- `honeypot_enabled`: option to disable the honeypot field at the application level. By default, true. - `secret`: customize the secret key to encode some internal values. By default, it reads the environment variable `ENV['INVISIBLE_CAPTCHA_SECRET']` and fallbacks to random value. Be careful, if you are running multiple Rails instances behind a load balancer, use always the same value via the environment variable. To change these defaults, add the following to an initializer (recommended `config/initializers/invisible_captcha.rb`): From 8f54f2afa9ed4274509b80abc3c5d20fac8b1c48 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Wed, 10 Jun 2026 00:06:32 -0700 Subject: [PATCH 7/8] improve --- lib/invisible_captcha/view_helpers.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/invisible_captcha/view_helpers.rb b/lib/invisible_captcha/view_helpers.rb index 96a95b8..5ae0072 100644 --- a/lib/invisible_captcha/view_helpers.rb +++ b/lib/invisible_captcha/view_helpers.rb @@ -33,12 +33,13 @@ def invisible_captcha_styles private def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) - if honeypot.is_a?(Hash) - options = honeypot - honeypot = nil - end - + return '' unless InvisibleCaptcha.honeypot_enabled || InvisibleCaptcha.spinner_enabled + if InvisibleCaptcha.honeypot_enabled + if honeypot.is_a?(Hash) + options = honeypot + honeypot = nil + end honeypot = honeypot ? honeypot.to_s : InvisibleCaptcha.get_honeypot label = options.delete(:sentence_for_humans) || InvisibleCaptcha.sentence_for_humans css_class = "#{honeypot}_#{Time.zone.now.to_i}" From bf4eee3b5fbf78129ee2b07be76edaeda07d4a46 Mon Sep 17 00:00:00 2001 From: John Bachir Date: Wed, 10 Jun 2026 00:11:44 -0700 Subject: [PATCH 8/8] Strip trailing whitespace + return ''.html_safe for output-buffer parity --- lib/invisible_captcha/view_helpers.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/invisible_captcha/view_helpers.rb b/lib/invisible_captcha/view_helpers.rb index 5ae0072..73cac5f 100644 --- a/lib/invisible_captcha/view_helpers.rb +++ b/lib/invisible_captcha/view_helpers.rb @@ -33,8 +33,8 @@ def invisible_captcha_styles private def build_invisible_captcha(honeypot = nil, scope = nil, options = {}) - return '' unless InvisibleCaptcha.honeypot_enabled || InvisibleCaptcha.spinner_enabled - + return ''.html_safe unless InvisibleCaptcha.honeypot_enabled || InvisibleCaptcha.spinner_enabled + if InvisibleCaptcha.honeypot_enabled if honeypot.is_a?(Hash) options = honeypot