Browse Source

#464: Stop Forum Spam, checks framework

Rafał Pitoń 10 years ago
parent
commit
ed7b936853

+ 1 - 0
docs/developers/index.rst

@@ -50,4 +50,5 @@ Following references cover everything you want to know about writing your own ap
    thread_store
    user_sites
    validators
+   validating_registrations
    views_errors

+ 19 - 0
docs/developers/settings.rst

@@ -246,6 +246,13 @@ MISAGO_MARKUP_EXTENSIONS
 List of python modules extending Misago markup.
 
 
+MISAGO_NEW_REGISTRATIONS_VALIDATORS
+-----------------------------------
+
+List of functions to be called when somebody attempts to register on forums using registration form.
+
+
+
 MISAGO_NOTIFICATIONS_MAX_AGE
 ----------------------------
 
@@ -293,6 +300,18 @@ For example, defining ``MISAGO_SENDFILE_LOCATIONS_PATH = 'misago_served_internal
 ``/home/mysite/www/attachments/13_05/142123.rar`` => ``/misago_served_internals/attachments/13_05/142123.rar``
 
 
+MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE
+-------------------------------------
+
+Minimum confidence returned by `Stop Forum Spam <http://www.stopforumspam.com/>`_ for Misago to reject new registration and block IP address for 1 day.
+
+
+MISAGO_STOP_FORUM_SPAM_USE
+--------------------------
+
+This settings allows you to decide wheter of not `Stop Forum Spam <http://www.stopforumspam.com/>`_ database should be used to validate IPs and emails during new users registrations.
+
+
 password_complexity
 -------------------
 

+ 17 - 0
docs/developers/validating_registrations.rst

@@ -0,0 +1,17 @@
+========================
+Validating Registrations
+========================
+
+Misago implements simple framework for extending process of new user registration with additional checks.
+
+When user submits registration form with valid data, this data is then passed to functions defined in `MISAGO_NEW_REGISTRATIONS_VALIDATORS` setting.
+
+Each function is called with following arguments:
+
+* ``ip:`` IP address using form.
+* ``username:`` username for which new account will be created.
+* ``email:`` e-mail address for which new account will be created.
+
+If function decides to interrup registration process and thus stop user from registering account, it can raise `django.core.exceptions.PermissionDenied` exception, which will result in user receiving "403 Permission Denied" response from site, as well as having his IP address automatically banned for one day.
+
+If none of defined tests raised `PermissionDenied`, user account will be registered normally.

+ 1 - 0
docs/index.rst

@@ -39,4 +39,5 @@ Table of Contents
    developers/thread_store
    developers/user_sites
    developers/validators
+   developers/validating_registrations
    developers/views_errors

+ 8 - 0
misago/conf/defaults.py

@@ -241,6 +241,14 @@ AUTHENTICATION_BACKENDS = (
     'misago.users.authbackends.MisagoBackend',
 )
 
+MISAGO_NEW_REGISTRATIONS_VALIDATORS = (
+    'misago.users.validators.validate_with_sfs',
+)
+
+MISAGO_STOP_FORUM_SPAM_USE = True
+MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE = 80
+
+
 # How many e-mails should be sent in single step.
 # This is used for conserving memory usage when mailing many users at same time
 

+ 23 - 2
misago/users/bans.py

@@ -5,10 +5,12 @@ API for testing values for bans
 Calling this instead of Ban.objects.find_ban is preffered, if you don't want
 to use validate_X_banned validators
 """
-from datetime import date, datetime
+from datetime import date, datetime, timedelta
+
+from django.utils import timezone
 
 from misago.core import cachebuster
-from misago.users.models import Ban, BanCache
+from misago.users.models import BAN_IP, Ban, BanCache
 
 
 BAN_CACHE_SESSION_KEY = 'misago_ip_check'
@@ -147,3 +149,22 @@ def _hydrate_session_cache(ban_cache):
         hydrated['valid_until'] = expiration_datetime.date()
 
     return hydrated
+
+
+"""
+Utility for banning naughty IPs
+"""
+def ban_ip(ip, user_message=None, staff_message=None, length=None):
+    if length:
+        valid_until = (timezone.now() + timedelta(days=length)).date()
+    else:
+        valid_until = None
+
+    Ban.objects.create(
+        test=BAN_IP,
+        banned_value=ip,
+        user_message=user_message,
+        staff_message=staff_message,
+        valid_until=valid_until
+    )
+    Ban.objects.invalidate_cache()

+ 4 - 0
misago/users/tests/test_validators.py

@@ -142,3 +142,7 @@ class ValidateUsernameLengthTests(TestCase):
             validate_username_length('a' * (settings.username_length_min - 1))
         with self.assertRaises(ValidationError):
             validate_username_length('a' * (settings.username_length_max + 1))
+
+
+class TestRegistrationValidators(TestCase):
+    pass

+ 56 - 1
misago/users/validators.py

@@ -1,11 +1,16 @@
+from importlib import import_module
+import json
 import re
 
-from django.core.exceptions import ValidationError
+import requests
+
+from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.validators import validate_email as validate_email_content
 from django.utils.translation import ungettext, ugettext_lazy as _
 from django.contrib.auth import get_user_model
 
 from misago.conf import settings
+
 from misago.users.bans import get_email_ban, get_username_ban
 
 
@@ -108,3 +113,53 @@ def validate_username(value, exclude=None):
     validate_username_content(value)
     validate_username_available(value, exclude)
     validate_username_banned(value)
+
+
+"""
+New account validators
+"""
+SFS_API_URL = 'http://api.stopforumspam.org/api?email=%(email)s&ip=%(ip)s&f=json&confidence'  # noqa
+
+
+def validate_with_sfs(ip, username, email):
+    if settings.MISAGO_STOP_FORUM_SPAM_USE:
+        _real_validate_with_sfs(ip, email)
+
+
+def _real_validate_with_sfs(ip, email):
+    try:
+        r = requests.get(SFS_API_URL % {'email': email, 'ip': ip},
+                         timeout=3)
+        api_response = json.loads(r.content)
+        ip_score = api_response.get('ip', {}).get('confidence', 0)
+        email_score = api_response.get('email', {}).get('confidence', 0)
+
+        api_score = max((ip_score, email_score))
+
+        if api_score > settings.MISAGO_STOP_FORUM_SPAM_MIN_CONFIDENCE:
+            raise PermissionDenied()
+    except requests.exceptions.RequestException:
+        pass # todo: log those somewhere
+
+
+"""
+Registration validation
+"""
+REGISTRATION_VALIDATORS = []
+
+
+def load_registration_validators():
+    for path in settings.MISAGO_NEW_REGISTRATIONS_VALIDATORS:
+        module = import_module('.'.join(path.split('.')[:-1]))
+        REGISTRATION_VALIDATORS.append(getattr(module, path.split('.')[-1]))
+
+
+if settings.MISAGO_NEW_REGISTRATIONS_VALIDATORS:
+    load_registration_validators()
+
+
+def validate_new_registration(ip, username, email, validators=None):
+    validators = validators or REGISTRATION_VALIDATORS
+
+    for validator in validators:
+        validator(ip, username, email)

+ 3 - 1
misago/users/views/auth.py

@@ -8,7 +8,8 @@ from django.views.decorators.debug import sensitive_post_parameters
 
 from misago.core.decorators import require_POST
 
-from misago.users.decorators import deny_authenticated, deny_guests
+from misago.users.decorators import (deny_authenticated, deny_guests,
+                                     deny_banned_ips)
 from misago.users.forms.auth import AuthenticationForm
 
 
@@ -16,6 +17,7 @@ from misago.users.forms.auth import AuthenticationForm
 @deny_authenticated
 @csrf_protect
 @never_cache
+@deny_banned_ips
 def login(request):
     form = AuthenticationForm(request)
 

+ 19 - 0
misago/users/views/register.py

@@ -1,7 +1,10 @@
 from django.contrib import messages
 from django.contrib.auth import authenticate, get_user_model, login
+from django.core.exceptions import PermissionDenied
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
+from django.utils import timezone
+from django.utils.formats import date_format
 from django.utils.translation import ugettext as _
 from django.views.decorators.cache import never_cache
 from django.views.decorators.debug import sensitive_post_parameters
@@ -10,11 +13,13 @@ from misago.conf import settings
 from misago.core.captcha import add_captcha_to_form
 from misago.core.mail import mail_user
 
+from misago.users.bans import ban_ip
 from misago.users.decorators import deny_authenticated, deny_banned_ips
 from misago.users.forms.register import RegisterForm
 from misago.users.models import (ACTIVATION_REQUIRED_USER,
                                  ACTIVATION_REQUIRED_ADMIN)
 from misago.users.tokens import make_activation_token
+from misago.users.validators import validate_new_registration
 
 
 def register_decorator(f):
@@ -38,6 +43,20 @@ def register(request):
     if request.method == 'POST':
         form = SecuredForm(request.POST)
         if form.is_valid():
+            try:
+                validate_new_registration(
+                    request.user.ip,
+                    form.cleaned_data['username'],
+                    form.cleaned_data['email'])
+            except PermissionDenied as e:
+                staff_message = _("This ban was automatically imposed on "
+                                  "%(date)s due to denied register attempt.")
+
+                message_formats = {'date': date_format(timezone.now())}
+                staff_message = staff_message % message_formats
+                ban_ip(request.user.ip, staff_message=staff_message, length=1)
+                raise e
+
             activation_kwargs = {}
             if settings.account_activation == 'user':
                 activation_kwargs = {

+ 2 - 0
runtests.py

@@ -34,6 +34,8 @@ def runtests():
             settings_file = settings_file.replace("{{ secret_key }}",
                                                   "t3stpr0j3ct")
             settings_file += """
+MISAGO_NEW_REGISTRATIONS_VALIDATORS = ()
+
 EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
 CACHES = {
     'default': {