diff --git a/app-rails/app/controllers/users/passwords_controller.rb b/app-rails/app/controllers/users/passwords_controller.rb
index 3f262b6..7068486 100644
--- a/app-rails/app/controllers/users/passwords_controller.rb
+++ b/app-rails/app/controllers/users/passwords_controller.rb
@@ -9,7 +9,8 @@ def forgot
def send_reset_password_instructions
email = params[:users_forgot_password_form][:email]
- @form = Users::ForgotPasswordForm.new(email: email)
+ spam_trap = params[:users_forgot_password_form][:spam_trap]
+ @form = Users::ForgotPasswordForm.new(email: email, spam_trap: spam_trap)
if @form.invalid?
flash.now[:errors] = @form.errors.full_messages
@@ -59,6 +60,6 @@ def auth_service
end
def reset_password_params
- params.require(:users_reset_password_form).permit(:email, :code, :password)
+ params.require(:users_reset_password_form).permit(:email, :code, :password, :spam_trap)
end
end
diff --git a/app-rails/app/controllers/users/registrations_controller.rb b/app-rails/app/controllers/users/registrations_controller.rb
index 03746ab..5d1785f 100644
--- a/app-rails/app/controllers/users/registrations_controller.rb
+++ b/app-rails/app/controllers/users/registrations_controller.rb
@@ -77,7 +77,7 @@ def auth_service
end
def registration_params
- params.require(:users_registration_form).permit(:email, :password, :password_confirmation, :role)
+ params.require(:users_registration_form).permit(:email, :password, :password_confirmation, :role, :spam_trap)
end
def verify_account_params
diff --git a/app-rails/app/controllers/users/sessions_controller.rb b/app-rails/app/controllers/users/sessions_controller.rb
index 556f146..8f4a51c 100644
--- a/app-rails/app/controllers/users/sessions_controller.rb
+++ b/app-rails/app/controllers/users/sessions_controller.rb
@@ -94,7 +94,7 @@ def auth_service
def new_session_params
# If :users_new_session_form is renamed, make sure to also update it in
# cognito_authenticatable.rb otherwise login will not work.
- params.require(:users_new_session_form).permit(:email, :password)
+ params.require(:users_new_session_form).permit(:email, :password, :spam_trap)
end
# This is similar to the default Devise SessionController implementation
diff --git a/app-rails/app/forms/users/forgot_password_form.rb b/app-rails/app/forms/users/forgot_password_form.rb
index 3b26d30..10f262d 100644
--- a/app-rails/app/forms/users/forgot_password_form.rb
+++ b/app-rails/app/forms/users/forgot_password_form.rb
@@ -1,7 +1,9 @@
class Users::ForgotPasswordForm
include ActiveModel::Model
- attr_accessor :email
+ attr_accessor :email, :spam_trap
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
+
+ validates :spam_trap, absence: true
end
diff --git a/app-rails/app/forms/users/new_session_form.rb b/app-rails/app/forms/users/new_session_form.rb
index 6135b62..ccdd82c 100644
--- a/app-rails/app/forms/users/new_session_form.rb
+++ b/app-rails/app/forms/users/new_session_form.rb
@@ -3,8 +3,10 @@
class Users::NewSessionForm
include ActiveModel::Model
- attr_accessor :email, :password
+ attr_accessor :email, :password, :spam_trap
validates :email, :password, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? }
+
+ validates :spam_trap, absence: true
end
diff --git a/app-rails/app/forms/users/registration_form.rb b/app-rails/app/forms/users/registration_form.rb
index 250ac6d..414a901 100644
--- a/app-rails/app/forms/users/registration_form.rb
+++ b/app-rails/app/forms/users/registration_form.rb
@@ -3,10 +3,12 @@
class Users::RegistrationForm
include ActiveModel::Model
- attr_accessor :email, :password, :password_confirmation, :role
+ attr_accessor :email, :password, :password_confirmation, :role, :spam_trap
validates :email, :password, :role, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? }
validates :password, confirmation: true, if: -> { password.present? }
+
+ validates :spam_trap, absence: true
end
diff --git a/app-rails/app/forms/users/reset_password_form.rb b/app-rails/app/forms/users/reset_password_form.rb
index 5fdab55..3131b21 100644
--- a/app-rails/app/forms/users/reset_password_form.rb
+++ b/app-rails/app/forms/users/reset_password_form.rb
@@ -3,9 +3,11 @@
class Users::ResetPasswordForm
include ActiveModel::Model
- attr_accessor :email, :password, :code
+ attr_accessor :email, :password, :code, :spam_trap
validates :email, :password, :code, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, if: -> { email.present? }
validates :code, length: { is: 6 }, if: -> { code.present? }
+
+ validates :spam_trap, absence: true
end
diff --git a/app-rails/app/helpers/uswds_form_builder.rb b/app-rails/app/helpers/uswds_form_builder.rb
index 8d9ff9b..4a56053 100644
--- a/app-rails/app/helpers/uswds_form_builder.rb
+++ b/app-rails/app/helpers/uswds_form_builder.rb
@@ -78,6 +78,16 @@ def submit(value = nil, options = {})
super(value, options)
end
+ def honeypot_field
+ spam_trap_classes = "opacity-0 position-absolute z-bottom top-0 left-0 height-0 width-0"
+ label_text = "Do not fill in this field. It is an anti-spam measure."
+
+ @template.content_tag(:div, class: "usa-form-group #{spam_trap_classes}") do
+ label(:spam_trap, label_text, { tabindex: -1, class: "usa-label #{spam_trap_classes}" }) +
+ @template.text_field(@object_name, :spam_trap, { autocomplete: "false", tabindex: -1, class: "usa-input #{spam_trap_classes}" })
+ end
+ end
+
########################################
# Custom helpers
########################################
diff --git a/app-rails/app/views/users/passwords/forgot.html.erb b/app-rails/app/views/users/passwords/forgot.html.erb
index 6efd207..286b880 100644
--- a/app-rails/app/views/users/passwords/forgot.html.erb
+++ b/app-rails/app/views/users/passwords/forgot.html.erb
@@ -5,6 +5,7 @@
<%= us_form_with model: @form, url: users_forgot_password_path, local: true do |f| %>
<%= f.email_field :email, { autocomplete: "username" } %>
+ <%= f.honeypot_field %>
<%= f.submit t(".submit") %>
<% end %>
\ No newline at end of file
diff --git a/app-rails/app/views/users/passwords/reset.html.erb b/app-rails/app/views/users/passwords/reset.html.erb
index cf187f2..8cf3003 100644
--- a/app-rails/app/views/users/passwords/reset.html.erb
+++ b/app-rails/app/views/users/passwords/reset.html.erb
@@ -12,6 +12,7 @@
<%= t(".title") %>
<%= us_form_with model: @form, url: users_reset_password_path, local: true do |f| %>
+ <%= f.honeypot_field %>
<%= f.text_field :code, { autocomplete: "off", label: t('.code'), width: "md" } %>
<%= f.email_field :email, { autocomplete: "username" } %>
diff --git a/app-rails/app/views/users/registrations/new.html.erb b/app-rails/app/views/users/registrations/new.html.erb
index 292d383..5361e66 100644
--- a/app-rails/app/views/users/registrations/new.html.erb
+++ b/app-rails/app/views/users/registrations/new.html.erb
@@ -13,6 +13,7 @@
<%= us_form_with model: @form, url: users_registrations_path, local: true do |f| %>
<%= f.hidden_field :role %>
+ <%= f.honeypot_field %>
<%= f.email_field :email %>
<%= f.password_field :password, autocomplete: "new-password", id: "new-password", hint: t("users.password_hint") %>
diff --git a/app-rails/app/views/users/sessions/new.html.erb b/app-rails/app/views/users/sessions/new.html.erb
index bdafacd..ba29478 100644
--- a/app-rails/app/views/users/sessions/new.html.erb
+++ b/app-rails/app/views/users/sessions/new.html.erb
@@ -6,6 +6,7 @@
<%= us_form_with model: @form, url: new_user_session_path, local: true do |f| %>
+ <%= f.honeypot_field %>
<%= f.email_field :email, { autocomplete: "username" } %>
<%= f.password_field :password, {id: "password", autocomplete: "current-password"} %>
diff --git a/app-rails/config/locales/defaults/en.yml b/app-rails/config/locales/defaults/en.yml
index 8dad02d..7c588c6 100644
--- a/app-rails/config/locales/defaults/en.yml
+++ b/app-rails/config/locales/defaults/en.yml
@@ -46,6 +46,10 @@ en:
default: "%m/%d/%Y"
short: "%b %d"
long: "%B %d, %Y"
+ errors:
+ attributes:
+ spam_trap:
+ present: "This field should be left empty. It is intended to prevent bots from submitting spam using this form."
helpers:
submit:
create: "Submit"
diff --git a/app-rails/spec/controllers/users/passwords_controller_spec.rb b/app-rails/spec/controllers/users/passwords_controller_spec.rb
index ce0895c..1238866 100644
--- a/app-rails/spec/controllers/users/passwords_controller_spec.rb
+++ b/app-rails/spec/controllers/users/passwords_controller_spec.rb
@@ -44,6 +44,15 @@
expect(response.status).to eq(422)
end
+
+ it "handles submission by bots" do
+ post :send_reset_password_instructions, params: {
+ users_forgot_password_form: { email: "UsernameExists@example.com", spam_trap: "I am a bot" },
+ locale: "en"
+ }
+
+ expect(response.status).to eq(422)
+ end
end
describe "GET reset" do
@@ -95,5 +104,19 @@
expect(response.status).to eq(422)
end
+
+ it "handles submission by bots" do
+ post :confirm_reset, params: {
+ users_reset_password_form: {
+ email: "test@example.com",
+ code: "123456",
+ password: "password",
+ spam_trap: "I am a bot"
+ },
+ locale: "en"
+ }
+
+ expect(response.status).to eq(422)
+ end
end
end
diff --git a/app-rails/spec/controllers/users/registrations_controller_spec.rb b/app-rails/spec/controllers/users/registrations_controller_spec.rb
index 1043e88..b7c1cd6 100644
--- a/app-rails/spec/controllers/users/registrations_controller_spec.rb
+++ b/app-rails/spec/controllers/users/registrations_controller_spec.rb
@@ -71,6 +71,22 @@
expect(response.status).to eq(422)
end
+
+ it "handles submission by bots" do
+ email = "test@example.com"
+
+ post :create, params: {
+ users_registration_form: {
+ email: email,
+ password: "password",
+ role: "employer",
+ spam_trap: "I am a bot"
+ },
+ locale: "en"
+ }
+
+ expect(response.status).to eq(422)
+ end
end
describe "GET new_account_verification" do
diff --git a/app-rails/spec/controllers/users/sessions_controller_spec.rb b/app-rails/spec/controllers/users/sessions_controller_spec.rb
index 94bf528..e05124a 100644
--- a/app-rails/spec/controllers/users/sessions_controller_spec.rb
+++ b/app-rails/spec/controllers/users/sessions_controller_spec.rb
@@ -91,6 +91,21 @@
expect(session[:challenge_email]).to eq("mfa@example.com")
expect(response).to redirect_to(session_challenge_path)
end
+
+ it "handles submission by bots" do
+ create(:user, uid: uid)
+
+ post :create, params: {
+ users_new_session_form: {
+ email: "test@example.com",
+ password: "password",
+ spam_trap: "I am a bot"
+ },
+ locale: "en"
+ }
+
+ expect(response.status).to eq(422)
+ end
end
describe "GET challenge" do
diff --git a/app-rails/spec/forms/users/new_session_form_spec.rb b/app-rails/spec/forms/users/new_session_form_spec.rb
index f9b6107..80c6f55 100644
--- a/app-rails/spec/forms/users/new_session_form_spec.rb
+++ b/app-rails/spec/forms/users/new_session_form_spec.rb
@@ -29,4 +29,15 @@
expect(form).not_to be_valid
expect(form.errors.of_kind?(:email, :invalid)).to be_truthy
end
+
+ it "requires the honeypot field to be empty" do
+ form = Users::NewSessionForm.new(
+ email: "test@example.com",
+ password: "password",
+ spam_trap: "I am a bot"
+ )
+
+ expect(form).not_to be_valid
+ expect(form.errors.of_kind?(:spam_trap, :present)).to be_truthy
+ end
end
diff --git a/app-rails/spec/forms/users/registration_form_spec.rb b/app-rails/spec/forms/users/registration_form_spec.rb
index 33a6365..c2921b0 100644
--- a/app-rails/spec/forms/users/registration_form_spec.rb
+++ b/app-rails/spec/forms/users/registration_form_spec.rb
@@ -36,4 +36,13 @@
expect(form).not_to be_valid
expect(form.errors.of_kind?(:email, :invalid)).to be_truthy
end
+
+ it "requires the honeypot field is empty" do
+ form.email = "test@example.com"
+ form.password = valid_password
+ form.spam_trap = "I am a bot"
+
+ expect(form).not_to be_valid
+ expect(form.errors.of_kind?(:spam_trap, :present)).to be_truthy
+ end
end
diff --git a/docs/app-rails/application-security.md b/docs/app-rails/application-security.md
index eec0102..389f723 100644
--- a/docs/app-rails/application-security.md
+++ b/docs/app-rails/application-security.md
@@ -49,7 +49,7 @@ There is currently no file upload or download functionality at this time, so ple
- [x] Use a secondary verification when users change their password
- Note: Change password requires 6 digit code from email sent to user's email address.
- [ ] Require user's password when changing email.
-- [ ] Include honeypot fields and logic on Non logged in forms to catch bots that spam all fields (good resource: https://nedbatchelder.com/text/stopbots.html).
+- [x] Include honeypot fields and logic on Non logged in forms to catch bots that spam all fields (good resource: https://nedbatchelder.com/text/stopbots.html).
- [ ] Consider using Captcha on account creation, login, change password, and change email forms.
- Note: Captchas are often not accessible to screen readers and their use should be part of a UX discussion.
- [x] Filter log entries so they do not include passwords or secrets