From 2fcab70e3b17f6c9f38059758ae7a24f0d94c826 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 14 Feb 2022 15:37:35 +0100 Subject: [PATCH] Add very simple CAPTCHA to standalone customer registration form --- src/pretix/presale/checkoutflow.py | 1 + src/pretix/presale/forms/customer.py | 43 ++++++++++++++++++++++++++++ src/pretix/presale/views/customer.py | 1 + src/tests/presale/test_customer.py | 7 +++-- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 9584520f7..406756999 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -282,6 +282,7 @@ class CustomerStep(CartMixin, TemplateFlowStep): ), prefix='register', request=self.request, + standalone=False, ) for field in f.fields.values(): field._show_required = field.required diff --git a/src/pretix/presale/forms/customer.py b/src/pretix/presale/forms/customer.py index 716eb992c..66de4365a 100644 --- a/src/pretix/presale/forms/customer.py +++ b/src/pretix/presale/forms/customer.py @@ -21,6 +21,7 @@ # import hashlib import ipaddress +import random from django import forms from django.conf import settings @@ -29,6 +30,7 @@ from django.contrib.auth.password_validation import ( password_validators_help_texts, validate_password, ) from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.core import signing from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from phonenumber_field.formfields import PhoneNumberField @@ -140,6 +142,8 @@ class RegistrationForm(forms.Form): def __init__(self, request=None, *args, **kwargs): 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) event = getattr(request, "event", None) @@ -163,6 +167,32 @@ class RegistrationForm(forms.Form): 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 def ratelimit_key(self): if not settings.HAS_REDIS: @@ -194,6 +224,19 @@ class RegistrationForm(forms.Form): 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'): raise forms.ValidationError( {'email': self.error_messages['required']}, diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index bf2963016..59da6ea3c 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -203,6 +203,7 @@ class RegistrationView(RedirectBackMixin, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['request'] = self.request + kwargs['standalone'] = True return kwargs def get_success_url(self): diff --git a/src/tests/presale/test_customer.py b/src/tests/presale/test_customer.py index b037a2ed4..d7611de84 100644 --- a/src/tests/presale/test_customer.py +++ b/src/tests/presale/test_customer.py @@ -25,7 +25,7 @@ from decimal import Decimal from urllib.parse import parse_qs, urlparse 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.test import Client from django.utils.timezone import now @@ -71,10 +71,13 @@ def test_disabled(env, client): @pytest.mark.django_db def test_org_register(env, client): + signer = signing.TimestampSigner(salt='customer-registration-captcha-127.0.0.1') r = client.post('/bigevents/account/register', { 'email': 'john@example.org', 'name_parts_0': 'John Doe', - }) + 'challenge': signer.sign('1+2'), + 'response': '3', + }, REMOTE_ADDR='127.0.0.1') assert r.status_code == 302 assert len(djmail.outbox) == 1 with scopes_disabled():