mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
Add sockpuppet spammer detection. Automatically flag posts if they are from new users (registered less than 24 hours ago) at the same IP address and one of them started the topic.
This commit is contained in:
parent
c1ba41195e
commit
3c2c6ab24b
@ -73,6 +73,8 @@ class SiteSetting < ActiveRecord::Base
|
||||
setting(:num_users_to_block_new_user, 3)
|
||||
setting(:notify_mods_when_user_blocked, false)
|
||||
|
||||
setting(:flag_sockpuppets, true)
|
||||
|
||||
# used mainly for dev, force hostname for Discourse.base_url
|
||||
# You would usually use multisite for this
|
||||
setting(:force_hostname, '')
|
||||
|
@ -249,6 +249,10 @@ class User < ActiveRecord::Base
|
||||
self.password_hash == hash_password(password, salt)
|
||||
end
|
||||
|
||||
def new_user?
|
||||
created_at >= 24.hours.ago || trust_level == TrustLevel.levels[:newuser]
|
||||
end
|
||||
|
||||
def seen_before?
|
||||
last_seen_at.present?
|
||||
end
|
||||
|
@ -4,27 +4,55 @@ class SpamRulesEnforcer
|
||||
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
# The exclamation point means that this method may make big changes to posts and the user.
|
||||
def self.enforce!(user)
|
||||
SpamRulesEnforcer.new(user).enforce!
|
||||
# The exclamation point means that this method may make big changes to posts and users.
|
||||
def self.enforce!(arg)
|
||||
SpamRulesEnforcer.new(arg).enforce!
|
||||
end
|
||||
|
||||
def initialize(arg)
|
||||
@user = arg if arg.is_a?(User)
|
||||
@post = arg if arg.is_a?(Post)
|
||||
end
|
||||
|
||||
def enforce!
|
||||
# TODO: once rules are in their own classes, invoke them from here in priority order
|
||||
if @user
|
||||
block_user if block?
|
||||
end
|
||||
if @post
|
||||
flag_sockpuppet_users if SiteSetting.flag_sockpuppets and reply_is_from_sockpuppet?
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# TODO: move this sockpuppet code to its own class. We should be able to add more rules, like ActiveModel validators.
|
||||
def reply_is_from_sockpuppet?
|
||||
return false if @post.post_number and @post.post_number == 1
|
||||
|
||||
first_post = @post.topic.posts.by_post_number.first
|
||||
return false if first_post.user.nil?
|
||||
|
||||
!first_post.user.staff? and !@post.user.staff? and
|
||||
@post.user != first_post.user and
|
||||
@post.user.ip_address == first_post.user.ip_address and
|
||||
@post.user.new_user?
|
||||
end
|
||||
|
||||
def flag_sockpuppet_users
|
||||
system_user = Discourse.system_user
|
||||
PostAction.act(system_user, @post, PostActionType.types[:spam]) rescue PostAction::AlreadyActed
|
||||
if (first_post = @post.topic.posts.by_post_number.first).try(:user).try(:new_user?)
|
||||
PostAction.act(system_user, first_post, PostActionType.types[:spam]) rescue PostAction::AlreadyActed
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: move all this auto-block code to another class:
|
||||
def self.block?(user)
|
||||
SpamRulesEnforcer.new(user).block?
|
||||
end
|
||||
|
||||
def self.punish!(user)
|
||||
SpamRulesEnforcer.new(user).punish_user
|
||||
end
|
||||
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def enforce!
|
||||
punish_user if block?
|
||||
true
|
||||
SpamRulesEnforcer.new(user).block_user
|
||||
end
|
||||
|
||||
def block?
|
||||
@ -46,7 +74,7 @@ class SpamRulesEnforcer
|
||||
PostAction.spam_flags.where(post_id: post_ids).uniq.pluck(:user_id).size
|
||||
end
|
||||
|
||||
def punish_user
|
||||
def block_user
|
||||
Post.transaction do
|
||||
if UserBlocker.block(@user, nil, {message: :too_many_spam_flags}) and SiteSetting.notify_mods_when_user_blocked
|
||||
GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, {user: @user, limit_once_per: false})
|
||||
|
@ -537,6 +537,7 @@ en:
|
||||
num_flags_to_block_new_user: "If a new user's posts get this many spam flags from (n) different users, hide all their posts and prevent future posting. 0 disables this feature."
|
||||
num_users_to_block_new_user: "If a new user's posts get (x) spam flags from this many different users, hide all their posts and prevent future posting. 0 disables this feature."
|
||||
notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators."
|
||||
flag_sockpuppets: "If a new user (i.e., registered in the last 24 hours) who started a topic and a new user who replies in that topic are at the same IP address, both their posts will automatically be flagged as spam."
|
||||
|
||||
traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak"
|
||||
post_undo_action_window_mins: "Number of seconds users are allowed to reverse actions on a post (like, flag, etc)"
|
||||
|
@ -76,6 +76,8 @@ class PostCreator
|
||||
{ user: @user,
|
||||
limit_once_per: 24.hours,
|
||||
message_params: {domains: @post.linked_hosts.keys.join(', ')} } )
|
||||
else
|
||||
SpamRulesEnforcer.enforce!(@post)
|
||||
end
|
||||
|
||||
enqueue_jobs
|
||||
|
@ -8,6 +8,7 @@ Fabricator(:user) do
|
||||
password 'myawesomepassword'
|
||||
trust_level TrustLevel.levels[:basic]
|
||||
bio_raw "I'm batman!"
|
||||
ip_address { sequence(:ip_address) { |i| "99.232.23.#{i%254}"} }
|
||||
end
|
||||
|
||||
Fabricator(:coding_horror, from: :user) do
|
||||
|
59
spec/integration/same_ip_spammers_spec.rb
Normal file
59
spec/integration/same_ip_spammers_spec.rb
Normal file
@ -0,0 +1,59 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe SpamRulesEnforcer do
|
||||
|
||||
Given(:ip_address) { '182.189.119.174' }
|
||||
Given!(:spammer1) { Fabricate(:user, ip_address: ip_address) }
|
||||
Given!(:spammer2) { Fabricate(:user, ip_address: ip_address) }
|
||||
Given(:spammer3) { Fabricate(:user, ip_address: ip_address) }
|
||||
|
||||
context 'flag_sockpuppets is disabled' do
|
||||
Given { SiteSetting.stubs(:flag_sockpuppets).returns(false) }
|
||||
Given!(:first_post) { create_post(user: spammer1) }
|
||||
Given!(:second_post) { create_post(user: spammer2, topic: first_post.topic) }
|
||||
|
||||
Then { first_post.reload.spam_count.should == 0 }
|
||||
And { second_post.reload.spam_count.should == 0 }
|
||||
end
|
||||
|
||||
context 'flag_sockpuppets is enabled' do
|
||||
Given { SiteSetting.stubs(:flag_sockpuppets).returns(true) }
|
||||
|
||||
context 'first spammer starts a topic' do
|
||||
Given!(:first_post) { create_post(user: spammer1) }
|
||||
|
||||
context 'second spammer replies' do
|
||||
Given!(:second_post) { create_post(user: spammer2, topic: first_post.topic) }
|
||||
|
||||
Then { first_post.reload.spam_count.should == 1 }
|
||||
And { second_post.reload.spam_count.should == 1 }
|
||||
|
||||
context 'third spam post' do
|
||||
Given!(:third_post) { create_post(user: spammer3, topic: first_post.topic) }
|
||||
|
||||
Then { first_post.reload.spam_count.should == 1 }
|
||||
And { second_post.reload.spam_count.should == 1 }
|
||||
And { third_post.reload.spam_count.should == 1 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'first user is not new' do
|
||||
Given!(:old_user) { Fabricate(:user, ip_address: ip_address, created_at: 2.days.ago, trust_level: TrustLevel.levels[:basic]) }
|
||||
|
||||
context 'first user starts a topic' do
|
||||
Given!(:first_post) { create_post(user: old_user) }
|
||||
|
||||
context 'a reply by a new user at the same IP address' do
|
||||
Given!(:second_post) { create_post(user: spammer2, topic: first_post.topic) }
|
||||
|
||||
Then { first_post.reload.spam_count.should == 0 }
|
||||
And { second_post.reload.spam_count.should == 1 }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
@ -2,8 +2,9 @@
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe PostAction do
|
||||
describe SpamRulesEnforcer do
|
||||
|
||||
describe 'auto-blocking users based on flagging' do
|
||||
before do
|
||||
SiteSetting.stubs(:flags_required_to_hide_post).returns(0) # never
|
||||
SiteSetting.stubs(:num_flags_to_block_new_user).returns(2)
|
||||
@ -102,4 +103,5 @@ describe PostAction do
|
||||
Then { expect(spam_post.reload).to_not be_hidden }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,83 @@ describe SpamRulesEnforcer do
|
||||
SystemMessage.stubs(:create)
|
||||
end
|
||||
|
||||
context 'flagging posts based on IP address of users' do
|
||||
describe 'reply_is_from_sockpuppet?' do
|
||||
let(:user1) { Fabricate(:user, ip_address: '182.189.119.174') }
|
||||
let(:post1) { Fabricate(:post, user: user1, topic: Fabricate(:topic, user: user1)) }
|
||||
|
||||
it 'is false for the first post in a topic' do
|
||||
SpamRulesEnforcer.new(post1).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is false if users have different IP addresses' do
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: '182.189.199.199'), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is true if users have the same IP address' do
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: '182.189.119.174'), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(true)
|
||||
end
|
||||
|
||||
it 'is false if reply and first post are from the same user' do
|
||||
post2 = Fabricate(:post, user: user1, topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is false if first post user is staff' do
|
||||
staff1 = Fabricate(:admin, ip_address: '182.189.119.174')
|
||||
staff_post1 = Fabricate(:post, user: staff1, topic: Fabricate(:topic, user: staff1))
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: staff1.ip_address), topic: staff_post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is false if second post user is staff' do
|
||||
post2 = Fabricate(:post, user: Fabricate(:moderator, ip_address: user1.ip_address), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is false if both users are staff' do
|
||||
staff1 = Fabricate(:moderator, ip_address: '182.189.119.174')
|
||||
staff_post1 = Fabricate(:post, user: staff1, topic: Fabricate(:topic, user: staff1))
|
||||
post2 = Fabricate(:post, user: Fabricate(:admin, ip_address: staff1.ip_address), topic: staff_post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is true if first post user was created over 24 hours ago and has trust level higher than 0' do
|
||||
old_user = Fabricate(:user, ip_address: '182.189.119.174', created_at: 25.hours.ago, trust_level: TrustLevel.levels[:basic])
|
||||
first_post = Fabricate(:post, user: old_user, topic: Fabricate(:topic, user: old_user))
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: old_user.ip_address), topic: first_post.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(true)
|
||||
end
|
||||
|
||||
it 'is false if second post user was created over 24 hours ago and has trust level higher than 0' do
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: user1.ip_address, created_at: 25.hours.ago, trust_level: TrustLevel.levels[:basic]), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
|
||||
it 'is true if first post user was created less that 24 hours ago and has trust level higher than 0' do
|
||||
new_user = Fabricate(:user, ip_address: '182.189.119.174', created_at: 1.hour.ago, trust_level: TrustLevel.levels[:basic])
|
||||
first_post = Fabricate(:post, user: new_user, topic: Fabricate(:topic, user: new_user))
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: new_user.ip_address), topic: first_post.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(true)
|
||||
end
|
||||
|
||||
it 'is true if second user was created less that 24 hours ago and has trust level higher than 0' do
|
||||
post2 = Fabricate(:post, user: Fabricate(:user, ip_address: user1.ip_address, created_at: 23.hours.ago, trust_level: TrustLevel.levels[:basic]), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(true)
|
||||
end
|
||||
|
||||
# A weird case
|
||||
it 'is false when user is nil on first post' do
|
||||
post1.user = nil; post1.save!
|
||||
post2 = Fabricate(:post, user: Fabricate(:user), topic: post1.topic)
|
||||
SpamRulesEnforcer.new(post2).reply_is_from_sockpuppet?.should eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'auto-blocking users based on flags' do
|
||||
before do
|
||||
SiteSetting.stubs(:flags_required_to_hide_post).returns(0) # never
|
||||
SiteSetting.stubs(:num_flags_to_block_new_user).returns(2)
|
||||
@ -21,25 +98,25 @@ describe SpamRulesEnforcer do
|
||||
enforcer = SpamRulesEnforcer.new(basic_user)
|
||||
enforcer.expects(:num_spam_flags_against_user).never
|
||||
enforcer.expects(:num_users_who_flagged_spam_against_user).never
|
||||
enforcer.expects(:punish_user).never
|
||||
enforcer.expects(:block_user).never
|
||||
enforcer.enforce!
|
||||
end
|
||||
|
||||
it 'takes no action if not enough flags by enough users have been submitted' do
|
||||
subject.stubs(:block?).returns(false)
|
||||
subject.expects(:punish_user).never
|
||||
subject.expects(:block_user).never
|
||||
subject.enforce!
|
||||
end
|
||||
|
||||
it 'delivers punishment when there are enough flags from enough users' do
|
||||
subject.stubs(:block?).returns(true)
|
||||
subject.expects(:punish_user)
|
||||
subject.expects(:block_user)
|
||||
subject.enforce!
|
||||
end
|
||||
end
|
||||
|
||||
describe 'num_spam_flags_against_user' do
|
||||
before { SpamRulesEnforcer.any_instance.stubs(:punish_user) }
|
||||
before { SpamRulesEnforcer.any_instance.stubs(:block_user) }
|
||||
let(:post) { Fabricate(:post) }
|
||||
let(:enforcer) { SpamRulesEnforcer.new(post.user) }
|
||||
subject { enforcer.num_spam_flags_against_user }
|
||||
@ -66,7 +143,7 @@ describe SpamRulesEnforcer do
|
||||
end
|
||||
|
||||
describe 'num_users_who_flagged_spam_against_user' do
|
||||
before { SpamRulesEnforcer.any_instance.stubs(:punish_user) }
|
||||
before { SpamRulesEnforcer.any_instance.stubs(:block_user) }
|
||||
let(:post) { Fabricate(:post) }
|
||||
let(:enforcer) { SpamRulesEnforcer.new(post.user) }
|
||||
subject { enforcer.num_users_who_flagged_spam_against_user }
|
||||
@ -99,7 +176,7 @@ describe SpamRulesEnforcer do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'punish_user' do
|
||||
describe 'block_user' do
|
||||
let!(:admin) { Fabricate(:admin) } # needed for SystemMessage
|
||||
let(:user) { Fabricate(:user) }
|
||||
let!(:post) { Fabricate(:post, user: user) }
|
||||
@ -117,7 +194,7 @@ describe SpamRulesEnforcer do
|
||||
end
|
||||
|
||||
it 'prevents the user from making new posts' do
|
||||
subject.punish_user
|
||||
subject.block_user
|
||||
expect(Guardian.new(user).can_create_post?(nil)).to be_false
|
||||
end
|
||||
|
||||
@ -127,13 +204,13 @@ describe SpamRulesEnforcer do
|
||||
GroupMessage.expects(:create).with do |group, msg_type, params|
|
||||
group == Group[:moderators].name and msg_type == :user_automatically_blocked and params[:user].id == user.id
|
||||
end
|
||||
subject.punish_user
|
||||
subject.block_user
|
||||
end
|
||||
|
||||
it "doesn't send a pm to moderators if notify_mods_when_user_blocked is false" do
|
||||
SiteSetting.stubs(:notify_mods_when_user_blocked).returns(false)
|
||||
GroupMessage.expects(:create).never
|
||||
subject.punish_user
|
||||
subject.block_user
|
||||
end
|
||||
end
|
||||
|
||||
@ -144,7 +221,7 @@ describe SpamRulesEnforcer do
|
||||
|
||||
it "doesn't send a pm to moderators if the user is already blocked" do
|
||||
GroupMessage.expects(:create).never
|
||||
subject.punish_user
|
||||
subject.block_user
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -231,5 +308,6 @@ describe SpamRulesEnforcer do
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user