Add honeypot fields and minimum fill-out time for sign-up form (#15276)
* Add honeypot fields to limit non-specialized spam Add two honeypot fields: a fake website input and a fake password confirmation one. The label/placeholder/aria-label tells not to fill them, and they are hidden in CSS, so legitimate users should not fall into these. This should cut down on some non-Mastodon-specific spambots. * Require a 3 seconds delay before submitting the registration form * Fix tests * Move registration form time check to model validation * Give people a chance to clear the honeypot fields * Refactor honeypot translation strings Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
		| @@ -1,12 +1,15 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class AboutController < ApplicationController | ||||
|   include RegistrationSpamConcern | ||||
|  | ||||
|   layout 'public' | ||||
|  | ||||
|   before_action :require_open_federation!, only: [:show, :more] | ||||
|   before_action :set_body_classes, only: :show | ||||
|   before_action :set_instance_presenter | ||||
|   before_action :set_expires_in, only: [:show, :more, :terms] | ||||
|   before_action :set_expires_in, only: [:more, :terms] | ||||
|   before_action :set_registration_form_time, only: :show | ||||
|  | ||||
|   skip_before_action :require_functional!, only: [:more, :terms] | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   include Devise::Controllers::Rememberable | ||||
|   include RegistrationSpamConcern | ||||
|  | ||||
|   layout :determine_layout | ||||
|  | ||||
| @@ -13,6 +14,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   before_action :set_body_classes, only: [:new, :create, :edit, :update] | ||||
|   before_action :require_not_suspended!, only: [:update] | ||||
|   before_action :set_cache_headers, only: [:edit, :update] | ||||
|   before_action :set_registration_form_time, only: :new | ||||
|  | ||||
|   skip_before_action :require_functional!, only: [:edit, :update] | ||||
|  | ||||
| @@ -45,16 +47,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController | ||||
|   def build_resource(hash = nil) | ||||
|     super(hash) | ||||
|  | ||||
|     resource.locale      = I18n.locale | ||||
|     resource.invite_code = params[:invite_code] if resource.invite_code.blank? | ||||
|     resource.sign_up_ip  = request.remote_ip | ||||
|     resource.locale                 = I18n.locale | ||||
|     resource.invite_code            = params[:invite_code] if resource.invite_code.blank? | ||||
|     resource.registration_form_time = session[:registration_form_time] | ||||
|     resource.sign_up_ip             = request.remote_ip | ||||
|  | ||||
|     resource.build_account if resource.account.nil? | ||||
|   end | ||||
|  | ||||
|   def configure_sign_up_params | ||||
|     devise_parameter_sanitizer.permit(:sign_up) do |u| | ||||
|       u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement) | ||||
|       u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
							
								
								
									
										9
									
								
								app/controllers/concerns/registration_spam_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/controllers/concerns/registration_spam_concern.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| module RegistrationSpamConcern | ||||
|   extend ActiveSupport::Concern | ||||
|  | ||||
|   def set_registration_form_time | ||||
|     session[:registration_form_time] = Time.now.utc | ||||
|   end | ||||
| end | ||||
| @@ -280,6 +280,17 @@ function main() { | ||||
|       target.style.display = 'block'; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // Empty the honeypot fields in JS in case something like an extension | ||||
|   // automatically filled them. | ||||
|   delegate(document, '#registration_new_user,#new_user', 'submit', () => { | ||||
|     ['user_website', 'user_confirm_password', 'registration_user_website', 'registration_user_confirm_password'].forEach(id => { | ||||
|       const field = document.getElementById(id); | ||||
|       if (field) { | ||||
|         field.value = ''; | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| loadPolyfills() | ||||
|   | ||||
| @@ -354,6 +354,7 @@ code { | ||||
|   input[type=number], | ||||
|   input[type=email], | ||||
|   input[type=password], | ||||
|   input[type=url], | ||||
|   textarea { | ||||
|     box-sizing: border-box; | ||||
|     font-size: 16px; | ||||
| @@ -994,3 +995,10 @@ code { | ||||
|     flex-direction: row; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .input.user_confirm_password, | ||||
| .input.user_website { | ||||
|   &:not(.field_with_errors) { | ||||
|     display: none; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -89,6 +89,13 @@ class User < ApplicationRecord | ||||
|   validates_with EmailMxValidator, if: :validate_email_dns? | ||||
|   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create | ||||
|  | ||||
|   # Those are honeypot/antispam fields | ||||
|   attr_accessor :registration_form_time, :website, :confirm_password | ||||
|  | ||||
|   validates_with RegistrationFormTimeValidator, on: :create | ||||
|   validates :website, absence: true, on: :create | ||||
|   validates :confirm_password, absence: true, on: :create | ||||
|  | ||||
|   scope :recent, -> { order(id: :desc) } | ||||
|   scope :pending, -> { where(approved: false) } | ||||
|   scope :approved, -> { where(approved: true) } | ||||
|   | ||||
							
								
								
									
										9
									
								
								app/validators/registration_form_time_validator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/validators/registration_form_time_validator.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| class RegistrationFormTimeValidator < ActiveModel::Validator | ||||
|   REGISTRATION_FORM_MIN_TIME = 3.seconds.freeze | ||||
|  | ||||
|   def validate(user) | ||||
|     user.errors.add(:base, I18n.t('auth.too_fast')) if user.registration_form_time.present? && user.registration_form_time > REGISTRATION_FORM_MIN_TIME.ago | ||||
|   end | ||||
| end | ||||
| @@ -10,6 +10,9 @@ | ||||
|       = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: closed_registrations? | ||||
|       = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations? | ||||
|  | ||||
|       = f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: closed_registrations? | ||||
|       = f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations? | ||||
|  | ||||
|     - if approved_registrations? | ||||
|       .fields-group | ||||
|         = f.simple_fields_for :invite_request do |invite_request_fields| | ||||
|   | ||||
| @@ -24,6 +24,9 @@ | ||||
|  | ||||
|   .fields-group | ||||
|     = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } | ||||
|     = f.input :confirm_password, as: :string, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' } | ||||
|  | ||||
|   = f.input :website, as: :url, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' } | ||||
|  | ||||
|   - if approved_registrations? && !@invite.present? | ||||
|     .fields-group | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| - if object.errors.any? | ||||
|   .flash-message.alert#error_explanation | ||||
|     %strong= t('generic.validation_errors', count: object.errors.count) | ||||
| - object.errors[:base].each do |error| | ||||
|   .flash-message.alert | ||||
|     %strong= error | ||||
|   | ||||
| @@ -751,6 +751,7 @@ en: | ||||
|       functional: Your account is fully operational. | ||||
|       pending: Your application is pending review by our staff. This may take some time. You will receive an e-mail if your application is approved. | ||||
|       redirecting_to: Your account is inactive because it is currently redirecting to %{acct}. | ||||
|     too_fast: Form submitted too fast, try again. | ||||
|     trouble_logging_in: Trouble logging in? | ||||
|     use_security_key: Use security key | ||||
|   authorize_follow: | ||||
|   | ||||
| @@ -126,6 +126,7 @@ en: | ||||
|         expires_in: Expire after | ||||
|         fields: Profile metadata | ||||
|         header: Header | ||||
|         honeypot: "%{label} (do not fill in)" | ||||
|         inbox_url: URL of the relay inbox | ||||
|         irreversible: Drop instead of hide | ||||
|         locale: Interface language | ||||
|   | ||||
| @@ -82,6 +82,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do | ||||
|   describe 'POST #create' do | ||||
|     let(:accept_language) { Rails.application.config.i18n.available_locales.sample.to_s } | ||||
|  | ||||
|     before do | ||||
|       session[:registration_form_time] = 5.seconds.ago | ||||
|     end | ||||
|  | ||||
|     around do |example| | ||||
|       current_locale = I18n.locale | ||||
|       example.run | ||||
|   | ||||
		Reference in New Issue
	
	Block a user