This commit is contained in:
Iliyan Angelov
2025-11-26 22:32:20 +02:00
commit ed94dd22dd
150 changed files with 14058 additions and 0 deletions

141
accounts/form_mixins.py Normal file
View File

@@ -0,0 +1,141 @@
"""
Form mixins for bot protection and validation.
"""
from django import forms
from django.core.cache import cache
from django.utils import timezone
from datetime import timedelta
import time
class HoneypotMixin:
"""
Honeypot field mixin - adds a hidden field that bots will fill but humans won't.
"""
# This field should be left empty by real users
website = forms.CharField(
required=False,
widget=forms.HiddenInput(attrs={'tabindex': '-1', 'autocomplete': 'off'}),
label='', # Empty label so screen readers skip it
)
def clean_website(self):
"""If this field is filled, it's likely a bot."""
website = self.cleaned_data.get('website')
if website:
raise forms.ValidationError('Bot detected. Please try again.')
return website
class TimeBasedValidationMixin:
"""
Time-based validation - prevents forms from being submitted too quickly (bot behavior).
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add a hidden timestamp field
self.fields['form_timestamp'] = forms.CharField(
required=False,
widget=forms.HiddenInput(),
initial=str(time.time())
)
def clean_form_timestamp(self):
"""Validate that form wasn't submitted too quickly."""
timestamp = self.cleaned_data.get('form_timestamp')
if not timestamp:
# If timestamp is missing, it might be a bot
raise forms.ValidationError('Invalid form submission.')
try:
submit_time = float(timestamp)
current_time = time.time()
elapsed = current_time - submit_time
# Forms submitted in less than 2 seconds are likely bots
if elapsed < 2:
raise forms.ValidationError('Form submitted too quickly. Please take your time.')
# Forms submitted after 1 hour are likely stale
if elapsed > 3600:
raise forms.ValidationError('Form session expired. Please refresh and try again.')
except (ValueError, TypeError):
raise forms.ValidationError('Invalid form submission.')
return timestamp
class RateLimitMixin:
"""
Rate limiting mixin - prevents too many form submissions from the same IP/user.
"""
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
def clean(self):
cleaned_data = super().clean()
if self.request:
# Get client IP
ip = self.get_client_ip(self.request)
# Create a unique key for this form type
form_name = self.__class__.__name__
cache_key = f'form_submission_{form_name}_{ip}'
# Check submission count
submissions = cache.get(cache_key, 0)
# Limit: 10 submissions per hour per IP
if submissions >= 10:
raise forms.ValidationError(
'Too many submissions. Please wait before submitting again.'
)
# Increment counter
cache.set(cache_key, submissions + 1, 3600) # 1 hour
return cleaned_data
def get_client_ip(self, request):
"""Get client IP address."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class BrowserFingerprintMixin:
"""
Browser fingerprint validation - ensures form is submitted from a real browser.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['user_agent_hash'] = forms.CharField(
required=False,
widget=forms.HiddenInput()
)
def clean_user_agent_hash(self):
"""Validate user agent is present and reasonable."""
ua_hash = self.cleaned_data.get('user_agent_hash')
# If no user agent hash, it might be a bot
if not ua_hash:
raise forms.ValidationError('Invalid browser signature.')
return ua_hash
class BotProtectionMixin(HoneypotMixin, TimeBasedValidationMixin, RateLimitMixin):
"""
Combined bot protection mixin that includes:
- Honeypot field
- Time-based validation
- Rate limiting
"""
pass