142 lines
4.5 KiB
Python
142 lines
4.5 KiB
Python
"""
|
|
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
|
|
|