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:
Neil Lalonde 2013-10-11 13:33:23 -04:00
parent c1ba41195e
commit 3c2c6ab24b
9 changed files with 472 additions and 295 deletions

View File

@ -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, '')

View File

@ -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

View File

@ -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})

View File

@ -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)"

View File

@ -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

View File

@ -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

View 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

View File

@ -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)
@ -103,3 +104,4 @@ describe PostAction do
end
end
end
end

View File

@ -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