From f998d02f1b90a7bf38b97820cb555e383da4b240 Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Sat, 11 Jul 2026 16:36:10 +0300 Subject: [PATCH 1/2] Upgrade spinner token comparison logic for hardened constant-time security --- lib/invisible_captcha/controller_ext.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/invisible_captcha/controller_ext.rb b/lib/invisible_captcha/controller_ext.rb index 015582b..cad5bb1 100644 --- a/lib/invisible_captcha/controller_ext.rb +++ b/lib/invisible_captcha/controller_ext.rb @@ -81,7 +81,7 @@ def spinner_spam? spinner_value = params[:spinner] session_value = session.delete(:invisible_captcha_spinner) - if spinner_value.blank? || spinner_value != session_value + if spinner_value.blank? || !secure_compare(spinner_value, session_value) warn_spam("Spinner value mismatch") return true end @@ -89,6 +89,10 @@ def spinner_spam? false end + def secure_compare(value, other_value) + ActiveSupport::SecurityUtils.secure_compare(value.to_s, other_value.to_s) + end + def honeypot_spam?(options = {}) return false unless InvisibleCaptcha.honeypot_enabled From 4efcbd53ec97c2907e5680c768349c349503f369 Mon Sep 17 00:00:00 2001 From: Artem Skrynnyk Date: Sat, 11 Jul 2026 18:07:30 +0300 Subject: [PATCH 2/2] Fix flaky timestamp specs --- spec/controllers_spec.rb | 18 +++++++++++++----- spec/view_helpers_spec.rb | 8 ++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/spec/controllers_spec.rb b/spec/controllers_spec.rb index 8c81bfa..396981a 100644 --- a/spec/controllers_spec.rb +++ b/spec/controllers_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.describe InvisibleCaptcha::ControllerExt, type: :controller do + include ActiveSupport::Testing::TimeHelpers + render_views before(:each) do @@ -43,7 +45,9 @@ end it 'fails if submission before timestamp_threshold' do - post :create, params: { topic: { title: 'foo' } } + freeze_time do + post :create, params: { topic: { title: 'foo' } } + end expect(response).to redirect_to 'http://test.host/topics' expect(flash[:error]).to eq(InvisibleCaptcha.timestamp_error_message) @@ -53,7 +57,9 @@ end it 'allows a custom on_timestamp_spam callback' do - put :update, params: { id: 1, topic: { title: 'bar' } } + freeze_time do + put :update, params: { id: 1, topic: { title: 'bar' } } + end expect(response.status).to eq(204) end @@ -66,9 +72,11 @@ def custom_timestamp_callback end end - expect { put :update, params: { id: 1, topic: { title: 'bar' } } } - .to change { session[:invisible_captcha_timestamp] } - .to be_present + freeze_time do + expect { put :update, params: { id: 1, topic: { title: 'bar' } } } + .to change { session[:invisible_captcha_timestamp] } + .to be_present + end end it 'runs on_spam callback if on_timestamp_spam callback is defined but passes' do diff --git a/spec/view_helpers_spec.rb b/spec/view_helpers_spec.rb index 90f11ec..8dd4fe1 100644 --- a/spec/view_helpers_spec.rb +++ b/spec/view_helpers_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.describe InvisibleCaptcha::ViewHelpers, type: :helper do + include ActiveSupport::Testing::TimeHelpers + before(:each) do allow(InvisibleCaptcha).to receive(:css_strategy).and_return("display:none;") allow_any_instance_of(ActionDispatch::ContentSecurityPolicy::Request).to receive(:content_security_policy_nonce).and_return('123') @@ -101,8 +103,10 @@ end it 'should set spam timestamp' do - invisible_captcha - expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601) + freeze_time do + invisible_captcha + expect(session[:invisible_captcha_timestamp]).to eq(Time.zone.now.iso8601) + end end context 'injectable_styles option' do