summaryrefslogtreecommitdiffstats
path: root/extensions/AntiSpam
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/AntiSpam')
-rw-r--r--extensions/AntiSpam/Config.pm21
-rw-r--r--extensions/AntiSpam/Extension.pm341
-rw-r--r--extensions/AntiSpam/lib/Config.pm54
-rw-r--r--extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl28
-rw-r--r--extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl16
-rw-r--r--extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl16
-rw-r--r--extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl13
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&amp;table=antispam_domain_blocklist">Domain Blocklist</a><br>
+ <a href="page.cgi?id=edit_table.html&amp;table=antispam_comment_blocklist">Comment Blocklist</a><br>
+ <a href="page.cgi?id=edit_table.html&amp;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 %]