feat: Enhances registration validation and cleanup

Adds rules for username and email validation, preventing the use of blocked keywords or patterns.

Implements a cleanup routine for expired IP, email, and username blocks, ensuring outdated data is removed.

Introduces IP rate-limiting in registration views to mitigate abuse and server strain.

Updates UI text for improved user clarity.

These changes aim to improve security, reduce spam registrations, and enhance user experience.
This commit is contained in:
Kumi 2024-12-21 14:41:00 +01:00
parent 64cab4d53c
commit 9c6555b348
Signed by: kumi
GPG key ID: ECBCC9082395383F
5 changed files with 121 additions and 14 deletions

View file

@ -1,12 +1,16 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from .models import UserRegistration
from .models import UserRegistration, EmailBlock, IPBlock, UsernameRule
admin.site.site_header = "Synapse Registration Administration"
admin.site.site_title = "Synapse Registration Administration"
admin.site.index_title = "Welcome to the Synapse Registration Administration"
admin.site.register(EmailBlock)
admin.site.register(IPBlock)
admin.site.register(UsernameRule)
admin.site.unregister(Group)

View file

@ -1,5 +1,10 @@
from django import forms
from django.conf import settings
from django.utils import timezone
import re
from .models import EmailBlock, UsernameRule, UserRegistration
class UsernameForm(forms.Form):
@ -25,9 +30,18 @@ class UsernameForm(forms.Form):
):
self.add_error(
"username",
"Username can only contain the characters a-z, 0-9, ., _, =, -, and /.",
"Sorry, your username can only contain the characters a-z, 0-9, ., _, =, -, and /.",
)
for rule in UsernameRule.objects.filter(expires__gt=timezone.now()):
regex = re.compile(rule.regex)
if regex.match(username):
self.add_error(
"username", "Sorry, the provided username cannot be used."
)
break
cleaned_data["username"] = username
return cleaned_data
@ -39,6 +53,26 @@ class EmailForm(forms.Form):
)
)
def clean(self):
cleaned_data = super().clean()
email = cleaned_data.get("email")
if UserRegistration.objects.filter(email=email).exists():
self.add_error(
"email", "You have recently registered with this email address."
)
for rule in EmailBlock.objects.filter(expires__gt=timezone.now()):
regex = re.compile(rule.regex)
if regex.match(email):
self.add_error(
"email", "Sorry, the provided email address/domain is blocked."
)
break
return cleaned_data
class RegistrationForm(forms.Form):
password1 = forms.CharField(
@ -58,7 +92,7 @@ class RegistrationForm(forms.Form):
widget=forms.Textarea(
attrs={
"class": "textarea",
"placeholder": "Why do you want to join our server? If you were referred by a current member, who referred you? If you found us through a different means, how did you find us?",
"placeholder": "Please tell us a little bit about yourself. Why do you want to join our server? If you were referred by a current member, who referred you? If you found us through a different means, how did you find us?",
}
),
)

View file

@ -1,12 +1,12 @@
from django.core.management.base import BaseCommand
from ...models import UserRegistration
from ...models import UserRegistration, IPBlock, EmailBlock, UsernameRule
from datetime import timedelta, datetime
class Command(BaseCommand):
help = "Clean up old user registrations"
help = "Clean up old user registrations and blocks"
def handle(self, *args, **options):
# Remove all registrations that are still in the "started" state after 48 hours
@ -17,10 +17,24 @@ class Command(BaseCommand):
# Remove all registrations that are denied or approved after 30 days
UserRegistration.objects.filter(
status__in=[UserRegistration.STATUS_DENIED, UserRegistration.STATUS_APPROVED],
status__in=[
UserRegistration.STATUS_DENIED,
UserRegistration.STATUS_APPROVED,
],
timestamp__lt=datetime.now() - timedelta(days=30),
).delete()
self.stdout.write(
self.style.SUCCESS("Successfully cleaned up old user registrations")
)
# Remove all IP blocks that have expired
IPBlock.objects.filter(expires__lt=datetime.now()).delete()
# Remove all email blocks that have expired
EmailBlock.objects.filter(expires__lt=datetime.now()).delete()
# Remove all username rules that have expired
UsernameRule.objects.filter(expires__lt=datetime.now()).delete()
self.stdout.write(self.style.SUCCESS("Successfully cleaned up old blocks"))

View file

@ -29,3 +29,39 @@ class UserRegistration(models.Model):
def __str__(self):
return self.username
class IPBlock(models.Model):
network = models.GenericIPAddressField()
netmask = models.SmallIntegerField(default=-1)
reason = models.TextField(null=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.netmask == -1:
self.netmask = 32 if self.network.version == 4 else 128
super().save(*args, **kwargs)
def __str__(self):
return f"{self.network}/{self.netmask}"
class EmailBlock(models.Model):
regex = models.CharField(max_length=1024)
reason = models.TextField(null=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.email
class UsernameRule(models.Model):
regex = models.CharField(max_length=1024)
reason = models.TextField(null=True, blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.regex

View file

@ -3,15 +3,31 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse_lazy
from django.core.mail import send_mail
from django.conf import settings
from django.utils import timezone
from .forms import UsernameForm, EmailForm, RegistrationForm
from .models import UserRegistration
from .models import UserRegistration, IPBlock
import requests
from secrets import token_urlsafe
from datetime import datetime, timedelta
from datetime import timedelta
from smtplib import SMTPRecipientsRefused
from ipaddress import ip_network
class RateLimitMixin:
def dispatch(self, request, *args, **kwargs):
if not settings.TRUST_PROXY:
ip_address = request.META.get("REMOTE_ADDR")
else:
ip_address = request.META.get("HTTP_X_FORWARDED_FOR")
for block in IPBlock.objects.filter(expiry__gt=timezone.now()):
if ip_network(ip_address) in ip_network(f"{block.network}/{block.netmask}"):
return render(request, "registration/ratelimit.html", status=429)
return super().dispatch(request, *args, **kwargs)
class LandingPageView(TemplateView):
@ -22,7 +38,7 @@ class ErrorPageView(TemplateView):
template_name = "error_page.html"
class CheckUsernameView(FormView):
class CheckUsernameView(RateLimitMixin, FormView):
template_name = "registration/username_form.html"
form_class = UsernameForm
success_url = reverse_lazy("email_input")
@ -42,7 +58,7 @@ class CheckUsernameView(FormView):
return self.form_invalid(form)
class EmailInputView(FormView):
class EmailInputView(RateLimitMixin, FormView):
template_name = "registration/email_form.html"
form_class = EmailForm
@ -69,7 +85,7 @@ class EmailInputView(FormView):
if (
UserRegistration.objects.filter(
ip_address=ip_address,
timestamp__gte=datetime.now() - timedelta(hours=24),
timestamp__gte=timezone.now() - timedelta(hours=24),
).count()
>= 5
):
@ -98,12 +114,15 @@ class EmailInputView(FormView):
return render(self.request, "registration/email_sent.html")
except SMTPRecipientsRefused:
form.add_error("email", "Your email address is invalid, not accepting mail, or blocked by our mail server.")
form.add_error(
"email",
"Your email address is invalid, not accepting mail, or blocked by our mail server.",
)
registration.delete()
return self.form_invalid(form)
class VerifyEmailView(View):
class VerifyEmailView(RateLimitMixin, View):
def get(self, request, token):
try:
registration = UserRegistration.objects.get(token=token)
@ -123,7 +142,7 @@ class VerifyEmailView(View):
return redirect("complete_registration")
class CompleteRegistrationView(FormView):
class CompleteRegistrationView(RateLimitMixin, FormView):
template_name = "registration/complete_registration.html"
form_class = RegistrationForm
success_url = reverse_lazy("registration_complete")