Add very simple CAPTCHA to standalone customer registration form

This commit is contained in:
Raphael Michel
2022-02-14 15:37:35 +01:00
parent 1414db35b7
commit 2fcab70e3b
4 changed files with 50 additions and 2 deletions

View File

@@ -282,6 +282,7 @@ class CustomerStep(CartMixin, TemplateFlowStep):
), ),
prefix='register', prefix='register',
request=self.request, request=self.request,
standalone=False,
) )
for field in f.fields.values(): for field in f.fields.values():
field._show_required = field.required field._show_required = field.required

View File

@@ -21,6 +21,7 @@
# #
import hashlib import hashlib
import ipaddress import ipaddress
import random
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@@ -29,6 +30,7 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password, password_validators_help_texts, validate_password,
) )
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core import signing
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
@@ -140,6 +142,8 @@ class RegistrationForm(forms.Form):
def __init__(self, request=None, *args, **kwargs): def __init__(self, request=None, *args, **kwargs):
self.request = request self.request = request
self.standalone = kwargs.pop('standalone')
self.signer = signing.TimestampSigner(salt=f'customer-registration-captcha-{get_client_ip(request)}')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
event = getattr(request, "event", None) event = getattr(request, "event", None)
@@ -163,6 +167,32 @@ class RegistrationForm(forms.Form):
label=_('Name'), label=_('Name'),
) )
if self.standalone:
# In the setandalone registration form, we add a simple CAPTCHA. We don't expect
# this to actually turn away and motivated attacker, it's mainly a protection
# against spam bots looking for contact forms. Since the standalone registration
# form is one of the simplest public forms we have in the application it is the
# most common target for untargeted bots.
a = random.randint(1, 9)
b = random.randint(1, 9)
if 'challenge' in self.data:
try:
a, b = self.signer.unsign(self.data.get('challenge'), max_age=3600).split('+')
a, b = int(a), int(b)
except:
pass
self.fields['challenge'] = forms.CharField(
widget=forms.HiddenInput,
initial=self.signer.sign(f'{a}+{b}')
)
self.fields['response'] = forms.IntegerField(
label=_('What is the result of {num1} + {num2}?').format(
num1=a, num2=b,
),
min_value=0,
)
@cached_property @cached_property
def ratelimit_key(self): def ratelimit_key(self):
if not settings.HAS_REDIS: if not settings.HAS_REDIS:
@@ -194,6 +224,19 @@ class RegistrationForm(forms.Form):
code='duplicate', code='duplicate',
) )
if self.standalone:
expect = -1
try:
a, b = self.signer.unsign(self.cleaned_data.get('challenge'), max_age=3600).split('+')
expect = int(a) + int(b)
except:
pass
if self.cleaned_data.get('response') != expect:
raise forms.ValidationError(
{'response': _('Please enter the correct result.')},
code='challenge_invalid'
)
if not self.cleaned_data.get('email'): if not self.cleaned_data.get('email'):
raise forms.ValidationError( raise forms.ValidationError(
{'email': self.error_messages['required']}, {'email': self.error_messages['required']},

View File

@@ -203,6 +203,7 @@ class RegistrationView(RedirectBackMixin, FormView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['request'] = self.request kwargs['request'] = self.request
kwargs['standalone'] = True
return kwargs return kwargs
def get_success_url(self): def get_success_url(self):

View File

@@ -25,7 +25,7 @@ from decimal import Decimal
from urllib.parse import parse_qs, urlparse from urllib.parse import parse_qs, urlparse
import pytest import pytest
from django.core import mail as djmail from django.core import mail as djmail, signing
from django.core.signing import dumps from django.core.signing import dumps
from django.test import Client from django.test import Client
from django.utils.timezone import now from django.utils.timezone import now
@@ -71,10 +71,13 @@ def test_disabled(env, client):
@pytest.mark.django_db @pytest.mark.django_db
def test_org_register(env, client): def test_org_register(env, client):
signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1')
r = client.post('/bigevents/account/register', { r = client.post('/bigevents/account/register', {
'email': 'john@example.org', 'email': 'john@example.org',
'name_parts_0': 'John Doe', 'name_parts_0': 'John Doe',
}) 'challenge': signer.sign('1+2'),
'response': '3',
}, REMOTE_ADDR='127.0.0.1')
assert r.status_code == 302 assert r.status_code == 302
assert len(djmail.outbox) == 1 assert len(djmail.outbox) == 1
with scopes_disabled(): with scopes_disabled():