From b7bd6c8fdd489652f9e8b50dcf1581b42a093992 Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 24 Mar 2020 02:18:37 -0500 Subject: [PATCH] users: require email verification for signups from proxies. Require users who signup using proxies to verify their email addresses before they can perform any edits. For verification purposes, the email must be a nondisposable address from a whitelist of trusted email providers. --- app/controllers/users_controller.rb | 1 + app/logical/email_validator.rb | 5 +++++ app/logical/ip_lookup.rb | 4 ++++ app/models/email_address.rb | 11 ++++++++++- app/models/user.rb | 2 ++ app/policies/application_policy.rb | 6 +++++- config/danbooru_default_config.rb | 7 +++++++ test/functional/emails_controller_test.rb | 24 +++++++++++++++++++++++ test/functional/posts_controller_test.rb | 7 +++++++ test/functional/users_controller_test.rb | 18 +++++++++++++++++ 10 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d61623db2..bdd99f2ac 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -61,6 +61,7 @@ class UsersController < ApplicationController def create @user = authorize User.new( last_ip_addr: CurrentUser.ip_addr, + requires_verification: IpLookup.new(CurrentUser.ip_addr).is_proxy?, name: params[:user][:name], password: params[:user][:password], password_confirmation: params[:user][:password_confirmation] diff --git a/app/logical/email_validator.rb b/app/logical/email_validator.rb index ada3d638e..7af1bbda3 100644 --- a/app/logical/email_validator.rb +++ b/app/logical/email_validator.rb @@ -76,6 +76,11 @@ module EmailValidator "#{name}@#{domain}" end + def nondisposable?(address) + domain = Mail::Address.new(address).domain + domain.in?(Danbooru.config.email_domain_verification_list) + end + def undeliverable?(to_address, from_address: Danbooru.config.contact_email, timeout: 3) mail_server = mx_domain(to_address, timeout: timeout) mail_server.nil? || rcpt_to_failed?(to_address, from_address, mail_server, timeout: timeout) diff --git a/app/logical/ip_lookup.rb b/app/logical/ip_lookup.rb index bc78e83b4..dc9f5bfce 100644 --- a/app/logical/ip_lookup.rb +++ b/app/logical/ip_lookup.rb @@ -5,6 +5,10 @@ class IpLookup attr_reader :ip_addr, :api_key, :cache_duration + def self.enabled? + Danbooru.config.ip_registry_api_key.present? + end + def initialize(ip_addr, api_key: Danbooru.config.ip_registry_api_key, cache_duration: 1.day) @ip_addr = ip_addr @api_key = api_key diff --git a/app/models/email_address.rb b/app/models/email_address.rb index d96470eff..c0f7779be 100644 --- a/app/models/email_address.rb +++ b/app/models/email_address.rb @@ -2,24 +2,33 @@ class EmailAddress < ApplicationRecord # https://www.regular-expressions.info/email.html EMAIL_REGEX = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/ - belongs_to :user + belongs_to :user, inverse_of: :email_address validates :address, presence: true, confirmation: true, format: { with: EMAIL_REGEX } validates :normalized_address, uniqueness: true validates :user_id, uniqueness: true validate :validate_deliverable, on: :deliverable + after_save :update_user def address=(value) self.normalized_address = EmailValidator.normalize(value) || address super end + def nondisposable? + EmailValidator.nondisposable?(address) + end + def validate_deliverable if EmailValidator.undeliverable?(address) errors[:address] << "is invalid or does not exist" end end + def update_user + user.update!(is_verified: is_verified? && nondisposable?) + end + concerning :VerificationMethods do def verifier @verifier ||= Danbooru::MessageVerifier.new(:email_verification_key) diff --git a/app/models/user.rb b/app/models/user.rb index 106392c7b..c3f2d2500 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,6 +63,8 @@ class User < ApplicationRecord opt_out_tracking no_flagging no_feedback + requires_verification + is_verified ) has_bit_flags BOOLEAN_ATTRIBUTES, :field => "bit_prefs" diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index a7d52ba37..b834eec02 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -39,7 +39,11 @@ class ApplicationPolicy end def unbanned? - user.is_member? && !user.is_banned? + user.is_member? && !user.is_banned? && verified? + end + + def verified? + user.is_verified? || user.is_gold? || !user.requires_verification? end def policy(object) diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index a9283db02..09edcebe2 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -519,6 +519,13 @@ module Danbooru nil end + # A list of email domains that are used for account verification purposes. + # If a user signs up from a proxy they will need to verify their account + # using an email address from one of the domains on this list. + def email_domain_verification_list + # ["gmail.com", "outlook.com", "yahoo.com"] + end + # API key for Google Maps. Used for embedding maps on IP address lookup pages. # Generate at https://console.developers.google.com/apis/credentials def google_maps_api_key diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb index 058025ed8..378b071fb 100644 --- a/test/functional/emails_controller_test.rb +++ b/test/functional/emails_controller_test.rb @@ -85,6 +85,30 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest assert_equal(false, @user.reload.email_address.is_verified) end end + + context "with a nondisposable email address" do + should "mark the user as verified" do + Danbooru.config.stubs(:email_domain_verification_list).returns(["gmail.com"]) + @user.email_address.update!(address: "test@gmail.com") + get email_verification_url(@user) + + assert_redirected_to @user + assert_equal(true, @user.reload.email_address.is_verified) + assert_equal(true, @user.is_verified) + end + end + + context "with a disposable email address" do + should "not mark the user as verified" do + Danbooru.config.stubs(:email_domain_verification_list).returns([]) + @user.email_address.update!(address: "test@mailinator.com") + get email_verification_url(@user) + + assert_redirected_to @user + assert_equal(true, @user.reload.email_address.is_verified) + assert_equal(false, @user.is_verified) + end + end end end end diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb index 2a3a6b499..5ff04d3e6 100644 --- a/test/functional/posts_controller_test.rb +++ b/test/functional/posts_controller_test.rb @@ -311,6 +311,13 @@ class PostsControllerTest < ActionDispatch::IntegrationTest assert_response 403 assert_not_equal("blah", @post.reload.tag_string) end + + should "not allow unverified users to update posts" do + @user.update!(requires_verification: true, is_verified: false) + put_auth post_path(@post), @user, params: { post: { tag_string: "blah" }} + assert_response 403 + assert_not_equal("blah", @post.reload.tag_string) + end end context "revert action" do diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index c002f7b6c..67a782d7e 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -151,6 +151,24 @@ class UsersControllerTest < ActionDispatch::IntegrationTest end end + should "mark users signing up from proxies as requiring verification" do + skip unless IpLookup.enabled? + self.remote_addr = "1.1.1.1" + post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }} + + assert_redirected_to User.last + assert_equal(true, User.last.requires_verification) + end + + should "not mark users signing up from non-proxies as requiring verification" do + skip unless IpLookup.enabled? + self.remote_addr = "187.37.226.17" + post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }} + + assert_redirected_to User.last + assert_equal(false, User.last.requires_verification) + end + context "with sockpuppet validation enabled" do setup do Danbooru.config.unstub(:enable_sock_puppet_validation?)