mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Add very simple CAPTCHA to standalone customer registration form
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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']},
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
Reference in New Issue
Block a user