users: improve sockpuppet detection on signup.

Require new accounts to verify their email address if any of the
following conditions are true:

* Their IP is a proxy.
* Their IP is under a partial IP ban.
* They're creating a new account while logged in to another account.
* Somebody recently created an account from the same IP in the last week.

Changes from before:

* Allow logged in users to view the signup page and create new accounts.
  Creating a new account while logged in to your old account is now
  allowed, but it requires email verification. This is a honeypot.
* Creating multiple accounts from the same IP is now allowed, but they
  require email verification. Previously the same IP check was only for
  the last day (now it's the last week), and only for an exact IP match
  (now it's a subnet match, /24 for IPv4 or /64 for IPv6).
* New account verification is disabled for private IPs (e.g. 127.0.0.1,
  192.168.0.1), to make development or running personal boorus easier
  (fixes #4618).
This commit is contained in:
evazion
2020-12-27 22:47:52 -06:00
parent 7e8f859b24
commit 9dc788c0ce
5 changed files with 115 additions and 42 deletions

View File

@@ -59,7 +59,7 @@ class UsersController < ApplicationController
end end
def create def create
requires_verification = IpLookup.new(CurrentUser.ip_addr).is_proxy? || IpBan.hit!(:partial, CurrentUser.ip_addr) requires_verification = UserVerifier.new(CurrentUser.user, request).requires_verification?
@user = authorize User.new( @user = authorize User.new(
last_ip_addr: CurrentUser.ip_addr, last_ip_addr: CurrentUser.ip_addr,

View File

@@ -1,3 +1,4 @@
require "danbooru/http/application_client"
require "danbooru/http/html_adapter" require "danbooru/http/html_adapter"
require "danbooru/http/xml_adapter" require "danbooru/http/xml_adapter"
require "danbooru/http/cache" require "danbooru/http/cache"

View File

@@ -0,0 +1,46 @@
# Checks whether a new account seems suspicious and should require email verification.
class UserVerifier
attr_reader :current_user, :request
# current_user is the user creating the new account, not the new account itself.
def initialize(current_user, request)
@current_user, @request = current_user, request
end
def requires_verification?
return false if is_local_ip?
# we check for IP bans first to make sure we bump the IP ban hit count
is_ip_banned? || is_logged_in? || is_recent_signup? || is_proxy?
end
private
def ip_address
@ip_address ||= IPAddress.parse(request.remote_ip)
end
def is_local_ip?
ip_address.loopback? || ip_address.link_local? || ip_address.private? || ip_address.try(:unique_local?)
end
def is_logged_in?
!current_user.is_anonymous?
end
def is_recent_signup?(age: 24.hours)
subnet_len = ip_address.ipv4? ? 24 : 64
subnet = "#{ip_address}/#{subnet_len}"
User.where("last_ip_addr <<= ?", subnet).where("created_at > ?", age.ago).exists?
end
def is_ip_banned?
IpBan.hit!(:partial, ip_address.to_s)
end
def is_proxy?
IpLookup.new(ip_address).is_proxy?
end
end

View File

@@ -1,6 +1,10 @@
class UserPolicy < ApplicationPolicy class UserPolicy < ApplicationPolicy
def create? def create?
user.is_anonymous? && !sockpuppet? true
end
def new?
true
end end
def update? def update?
@@ -31,10 +35,6 @@ class UserPolicy < ApplicationPolicy
user.is_admin? || record.id == user.id || !record.enable_private_favorites? user.is_admin? || record.id == user.id || !record.enable_private_favorites?
end end
def sockpuppet?
User.where(last_ip_addr: request.remote_ip).where("created_at > ?", 1.day.ago).exists?
end
def permitted_attributes_for_create def permitted_attributes_for_create
[:name, :password, :password_confirmation, { email_address_attributes: [:address] }] [:name, :password, :password_confirmation, { email_address_attributes: [:address] }]
end end

View File

@@ -243,6 +243,11 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
get new_user_path get new_user_path
assert_response :success assert_response :success
end end
should "render for a logged in user" do
get_auth new_user_path, @user
assert_response :success
end
end end
context "create action" do context "create action" do
@@ -256,11 +261,6 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_no_enqueued_emails assert_no_enqueued_emails
end end
should "not allow logged in users to create a new account" do
post_auth users_path, @user, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_response 403
end
should "create a user with a valid email" do should "create a user with a valid email" do
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1", email: "webmaster@danbooru.donmai.us" }} post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1", email: "webmaster@danbooru.donmai.us" }}
@@ -289,45 +289,71 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
end end
end end
should "mark users signing up from proxies as requiring verification" do context "sockpuppet detection" do
skip unless IpLookup.enabled? setup do
@private_ip = "192.168.0.1"
@valid_ip = "187.37.226.17" # a random valid, non-proxy public IP
@proxy_ip = "51.15.128.1"
end
self.remote_addr = "51.15.128.1" should "mark accounts created by already logged in users as requiring verification" do
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }} self.remote_addr = @valid_ip
assert_redirected_to User.last post_auth users_path, @user, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_equal(true, User.last.requires_verification)
end
should "mark users signing up from a partial banned IP as requiring verification" do assert_redirected_to User.last
skip unless IpLookup.enabled? assert_equal(true, User.last.requires_verification)
self.remote_addr = "187.37.226.17" end
@ip_ban = create(:ip_ban, ip_addr: self.remote_addr, category: :partial) should "mark users signing up from proxies as requiring verification" do
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }} skip unless IpLookup.enabled?
self.remote_addr = @proxy_ip
assert_redirected_to User.last post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_equal(true, User.last.requires_verification)
assert_equal(1, @ip_ban.reload.hit_count)
assert(@ip_ban.last_hit_at > 1.minute.ago)
end
should "not mark users signing up from non-proxies as requiring verification" do assert_redirected_to User.last
skip unless IpLookup.enabled? assert_equal(true, User.last.requires_verification)
self.remote_addr = "187.37.226.17" end
post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_redirected_to User.last should "mark users signing up from a partial banned IP as requiring verification" do
assert_equal(false, User.last.requires_verification) self.remote_addr = @valid_ip
end
context "with sockpuppet validation enabled" do @ip_ban = create(:ip_ban, ip_addr: self.remote_addr, category: :partial)
should "not allow registering multiple accounts with the same IP" do post users_path, params: { user: { name: "xxx", password: "xxxxx1", password_confirmation: "xxxxx1" }}
assert_difference("User.count", 0) do
@user.update(last_ip_addr: "127.0.0.1") assert_redirected_to User.last
post users_path, params: {:user => {:name => "dupe", :password => "xxxxx1", :password_confirmation => "xxxxx1"}} assert_equal(true, User.last.requires_verification)
assert_response 403 assert_equal(1, @ip_ban.reload.hit_count)
end assert(@ip_ban.last_hit_at > 1.minute.ago)
end
should "not mark users signing up from non-proxies as requiring verification" do
skip unless IpLookup.enabled?
self.remote_addr = @valid_ip
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
should "mark accounts registered from an IPv4 address recently used for another account as requiring verification" do
@user.update!(last_ip_addr: @valid_ip)
self.remote_addr = @valid_ip
post users_path, params: { user: { name: "dupe", 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 localhost as requiring verification" do
self.remote_addr = "127.0.0.1"
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 end
end end
end end