diff options
Diffstat (limited to 'extensions/AntiSpam')
7 files changed, 489 insertions, 0 deletions
diff --git a/extensions/AntiSpam/Config.pm b/extensions/AntiSpam/Config.pm new file mode 100644 index 000000000..92c32f629 --- /dev/null +++ b/extensions/AntiSpam/Config.pm @@ -0,0 +1,21 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::AntiSpam; +use strict; + +use constant NAME => 'AntiSpam'; +use constant REQUIRED_MODULES => [ + { + package => 'Email-Address', + module => 'Email::Address', + version => 0, + }, +]; +use constant OPTIONAL_MODULES => []; + +__PACKAGE__->NAME; diff --git a/extensions/AntiSpam/Extension.pm b/extensions/AntiSpam/Extension.pm new file mode 100644 index 000000000..40a637adc --- /dev/null +++ b/extensions/AntiSpam/Extension.pm @@ -0,0 +1,341 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::AntiSpam; + +use strict; +use warnings; + +use base qw(Bugzilla::Extension); + +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::Util qw(remote_ip trick_taint); +use Email::Address; +use Encode; +use Socket; +use Sys::Syslog qw(:DEFAULT setlogsock); + +our $VERSION = '1'; + +# +# project honeypot integration +# + +sub _project_honeypot_blocking { + my ($self, $api_key, $login) = @_; + my $ip = remote_ip(); + return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org"; + return unless my $packed = gethostbyname($lookup); + my $honeypot = inet_ntoa($packed); + return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my ($status, $days, $threat, $type) = ($1, $2, $3, $4); + + return if $status != 127 + || $threat < Bugzilla->params->{honeypot_threat_threshold}; + + _syslog(sprintf("[audit] blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot)); + ThrowUserError('account_creation_restricted'); +} + +sub config_modify_panels { + my ($self, $args) = @_; + push @{ $args->{panels}->{auth}->{params} }, { + name => 'honeypot_api_key', + type => 't', + default => '', + }; + push @{ $args->{panels}->{auth}->{params} }, { + name => 'honeypot_threat_threshold', + type => 't', + default => '32', + }; +} + +# +# comment blocking +# + +sub _comment_blocking { + my ($self, $params) = @_; + + # as we want to use this sparingly, we only block comments on bugs which + # the user didn't report, and skip it completely if the user is in the + # editbugs group. + my $user = Bugzilla->user; + return if $user->in_group('editbugs'); + # new bug + return unless $params->{bug_id}; + # existing bug + my $bug = ref($params->{bug_id}) + ? $params->{bug_id} + : Bugzilla::Bug->new($params->{bug_id}); + return if $bug->reporter->id == $user->id; + + my $blocklist = Bugzilla->dbh->selectcol_arrayref( + 'SELECT word FROM antispam_comment_blocklist' + ); + return unless @$blocklist; + + my $regex = '\b(?:' . join('|', map { quotemeta } @$blocklist) . ')\b'; + if ($params->{thetext} =~ /$regex/i) { + ThrowUserError('antispam_comment_blocked'); + } +} + +# +# domain blocking +# + +sub _domain_blocking { + my ($self, $login) = @_; + my $address = Email::Address->new(undef, $login); + my $blocked = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?", + undef, + $address->host + ); + if ($blocked) { + _syslog(sprintf("[audit] blocked <%s> from creating %s, blacklisted domain", remote_ip(), $login)); + ThrowUserError('account_creation_restricted'); + } +} + +# +# ip blocking +# + +sub _ip_blocking { + my ($self, $login) = @_; + my $ip = remote_ip(); + trick_taint($ip); + my $blocked = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?", + undef, + $ip + ); + if ($blocked) { + _syslog(sprintf("[audit] blocked <%s> from creating %s, blacklisted IP", $ip, $login)); + ThrowUserError('account_creation_restricted'); + } +} + +# +# spam user disabling +# + +sub comment_after_add_tag { + my ($self, $args) = @_; + return unless lc($args->{tag}) eq 'spam'; + my $comment = $args->{comment}; + my $author = $comment->author; + + # exclude disabled users + return if !$author->is_enabled; + + # exclude users by group + return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group}); + + # exclude users who are no longer new + return if !$author->is_new; + + # exclude users who haven't made enough comments + my $spam_count = Bugzilla->params->{antispam_spammer_comment_count}; + return if $author->comment_count < $spam_count; + + # get user's comments + my $comments = Bugzilla->dbh->selectall_arrayref(" + SELECT longdescs.comment_id,longdescs_tags.id + FROM longdescs + LEFT JOIN longdescs_tags + ON longdescs_tags.comment_id = longdescs.comment_id + AND longdescs_tags.tag = 'spam' + WHERE longdescs.who = ? + ORDER BY longdescs.bug_when + ", undef, $author->id); + + # this comment is spam + my $comment_id = $comment->id; + foreach my $ra (@$comments) { + if ($ra->[0] == $comment_id) { + $ra->[1] = 1; + last; + } + } + + # throw away comment id and negate bool to make it a list of not-spam + $comments = [ map { $_->[1] ? 0 : 1 } @$comments ]; + + my $reason; + + # check if the first N comments are spam + if (!scalar(grep { $_ } @$comments[0..($spam_count - 1)])) { + $reason = "first $spam_count comments are spam"; + } + + # check if the last N comments are spam + elsif (!scalar(grep { $_ } @$comments[-$spam_count..-1])) { + $reason = "last $spam_count comments are spam"; + } + + # disable + if ($reason) { + $author->set_disabledtext(Bugzilla->params->{antispam_spammer_disable_text}); + $author->set_disable_mail(1); + $author->update(); + _syslog(sprintf("[audit] antispam disabled <%s>: %s", $author->login, $reason)); + } +} + +# +# hooks +# + +sub object_end_of_create_validators { + my ($self, $args) = @_; + if ($args->{class} eq 'Bugzilla::Comment') { + $self->_comment_blocking($args->{params}); + } +} + +sub user_verify_login { + my ($self, $args) = @_; + if (my $api_key = Bugzilla->params->{honeypot_api_key}) { + $self->_project_honeypot_blocking($api_key, $args->{login}); + } + $self->_ip_blocking($args->{login}); + $self->_domain_blocking($args->{login}); +} + +sub editable_tables { + my ($self, $args) = @_; + my $tables = $args->{tables}; + # allow these tables to be edited with the EditTables extension + $tables->{antispam_domain_blocklist} = { + id_field => 'id', + order_by => 'domain', + blurb => 'List of fully qualified domain names to block at account creation time.', + group => 'can_configure_antispam', + }; + $tables->{antispam_comment_blocklist} = { + id_field => 'id', + order_by => 'word', + blurb => "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" . + "This only applies to comments on bugs which the user didn't report.\n" . + "Users in the editbugs group are exempt from comment blocking.", + group => 'can_configure_antispam', + }; + $tables->{antispam_ip_blocklist} = { + id_field => 'id', + order_by => 'ip_address', + blurb => 'List of IPv4 addresses which are prevented from creating accounts.', + group => 'can_configure_antispam', + }; +} + +sub config_add_panels { + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config"; +} + +# +# installation +# + +sub install_before_final_checks { + if (!Bugzilla::Group->new({ name => 'can_configure_antispam' })) { + Bugzilla::Group->create({ + name => 'can_configure_antispam', + description => 'Can configure Anti-Spam measures', + isbuggroup => 0, + }); + } +} + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'antispam_domain_blocklist'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + domain => { + TYPE => 'VARCHAR(255)', + NOTNULL => 1, + }, + comment => { + TYPE => 'VARCHAR(255)', + NOTNULL => 1, + }, + ], + INDEXES => [ + antispam_domain_blocklist_idx => { + FIELDS => [ 'domain' ], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'antispam_comment_blocklist'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + word => { + TYPE => 'VARCHAR(255)', + NOTNULL => 1, + }, + ], + INDEXES => [ + antispam_comment_blocklist_idx => { + FIELDS => [ 'word' ], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'antispam_ip_blocklist'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + ip_address => { + TYPE => 'VARCHAR(15)', + NOTNULL => 1, + }, + comment => { + TYPE => 'VARCHAR(255)', + NOTNULL => 1, + }, + ], + INDEXES => [ + antispam_ip_blocklist_idx => { + FIELDS => [ 'ip_address' ], + TYPE => 'UNIQUE', + }, + ], + }; +} + +# +# utilities +# + +sub _syslog { + my $message = shift; + openlog('apache', 'cons,pid', 'local4'); + syslog('notice', encode_utf8($message)); + closelog(); +} + +__PACKAGE__->NAME; diff --git a/extensions/AntiSpam/lib/Config.pm b/extensions/AntiSpam/lib/Config.pm new file mode 100644 index 000000000..dc3e2820f --- /dev/null +++ b/extensions/AntiSpam/lib/Config.pm @@ -0,0 +1,54 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::AntiSpam::Config; + +use strict; +use warnings; + +use Bugzilla::Config::Common; +use Bugzilla::Group; + +our $sortkey = 511; + +sub get_param_list { + my ($class) = @_; + + my @param_list = ( + { + name => 'antispam_spammer_exclude_group', + type => 's', + choices => \&_get_all_group_names, + default => 'canconfirm', + checker => \&check_group + }, + { + name => 'antispam_spammer_comment_count', + type => 't', + default => '3', + checker => \&check_numeric + }, + { + name => 'antispam_spammer_disable_text', + type => 'l', + default => + "This account has been automatically disabled as a result of a " . + "high number of spam comments.\n\nPlease contact the address at ". + "the end of this message if you believe this to be an error." + }, + ); + + return @param_list; +} + +sub _get_all_group_names { + my @group_names = map {$_->name} Bugzilla::Group->get_all; + unshift(@group_names, ''); + return \@group_names; +} + +1; diff --git a/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl b/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl new file mode 100644 index 000000000..45dae623a --- /dev/null +++ b/extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl @@ -0,0 +1,28 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% + title = "Anti-Spam" + desc = "Edit Anti-Spam Configuration" +%] + +[% param_descs = +{ + antispam_spammer_exclude_group => + "Users in this group will be excluded from automatic disabling." + + antispam_spammer_comment_count => + "If a user has made at least this many comments, and either their first " _ + "NNN comments or their last NNN comments have been tagged as spam, their " _ + "account will be automatically disabled." + + antispam_spammer_disable_text => + "This message will be displayed to the user when they try to log " _ + "in after their account is disabled." +} +%] diff --git a/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl new file mode 100644 index 000000000..e55475d98 --- /dev/null +++ b/extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl @@ -0,0 +1,16 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF user.in_group('can_configure_antispam') %] + <dt id="antispam" >AntiSpam</dt> + <dd> + <a href="page.cgi?id=edit_table.html&table=antispam_domain_blocklist">Domain Blocklist</a><br> + <a href="page.cgi?id=edit_table.html&table=antispam_comment_blocklist">Comment Blocklist</a><br> + <a href="page.cgi?id=edit_table.html&table=antispam_ip_blocklist">IP Address Blocklist</a><br> + </dd> +[% END %] diff --git a/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl b/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl new file mode 100644 index 000000000..e8e67eccb --- /dev/null +++ b/extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl @@ -0,0 +1,16 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF panel.name == "auth" %] + [% panel.param_descs.honeypot_api_key = + 'API Key for http://www.projecthoneypot.org' + %] + [% panel.param_descs.honeypot_threat_threshold = + 'Users will be unable to create accounts if their honeypot threat score is this value or higher.' + %] +[% END -%] diff --git a/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..44410ca2f --- /dev/null +++ b/extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,13 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF error == "antispam_comment_blocked" %] + [% title = "Comment Blocked" %] + Your comment contains one or more words deemed inappropriate for use by the + administrators of this site. +[% END %] |