update
This commit is contained in:
141
accounts/form_mixins.py
Normal file
141
accounts/form_mixins.py
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user