summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2020-12-10 06:27:26 +0100
committerGitHub <noreply@github.com>2020-12-10 06:27:26 +0100
commit49eb4d4ddf61e25c5aaab89aa630ddd3c7f3c23d (patch)
tree3ed700750271b0ac8892f2282325d9838b915446 /app
parent9669167aaeaa834dcc99fa7df961c4f9b8118850 (diff)
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>
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb5
-rw-r--r--app/controllers/auth/registrations_controller.rb11
-rw-r--r--app/controllers/concerns/registration_spam_concern.rb9
-rw-r--r--app/javascript/packs/public.js11
-rw-r--r--app/javascript/styles/mastodon/forms.scss8
-rw-r--r--app/models/user.rb7
-rw-r--r--app/validators/registration_form_time_validator.rb9
-rw-r--r--app/views/about/_registration.html.haml3
-rw-r--r--app/views/auth/registrations/new.html.haml3
-rw-r--r--app/views/shared/_error_messages.html.haml3
10 files changed, 64 insertions, 5 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index abd1ec0cb64..dcad5d3b44e 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -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]
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index eb092419020..a3114ab2532 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -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
diff --git a/app/controllers/concerns/registration_spam_concern.rb b/app/controllers/concerns/registration_spam_concern.rb
new file mode 100644
index 00000000000..af434c985a0
--- /dev/null
+++ b/app/controllers/concerns/registration_spam_concern.rb
@@ -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
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 39defa7ae9e..8c5c15b8f4c 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -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()
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index a54a5fdedf4..92d89e6f242 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -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;
+ }
+}
diff --git a/app/models/user.rb b/app/models/user.rb
index 94fee999ab3..981dc6d47cf 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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) }
diff --git a/app/validators/registration_form_time_validator.rb b/app/validators/registration_form_time_validator.rb
new file mode 100644
index 00000000000..ba7c7e6c646
--- /dev/null
+++ b/app/validators/registration_form_time_validator.rb
@@ -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
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 5d159e9e690..6160ca4d40f 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -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|
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index cc72b87ce36..de541847f2a 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -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
diff --git a/app/views/shared/_error_messages.html.haml b/app/views/shared/_error_messages.html.haml
index 28becd6c448..4916bd424e8 100644
--- a/app/views/shared/_error_messages.html.haml
+++ b/app/views/shared/_error_messages.html.haml
@@ -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