Projet

Général

Profil

0001-views-fix-sms-registration-phone-number-ratelimit-ke.patch

Paul Marillonnet, 19 décembre 2022 16:14

Télécharger (7,23 ko)

Voir les différences:

Subject: [PATCH] views: fix sms-registration phone-number ratelimit key
 (#72597)

 src/authentic2/app_settings.py |  4 +-
 src/authentic2/utils/sms.py    | 10 +++++
 src/authentic2/views.py        |  7 +--
 tests/test_views.py            | 78 ++++++++++++++++++++++++++++++++++
 4 files changed, 94 insertions(+), 5 deletions(-)
src/authentic2/app_settings.py
301 301
    A2_EMAILS_ADDRESS_RATELIMIT=Setting(
302 302
        default='3/d', definition='Maximum rate of emails sent to the same email address.'
303 303
    ),
304
    A2_SMS_RATELIMIT=Setting(
305
        default='1/h', definition='Maximum rate of SMSs sent to the same email address.'
304
    A2_SMS_NUMBER_RATELIMIT=Setting(
305
        default='10/h', definition='Maximum rate of SMSs sent to the same phone number.'
306 306
    ),
307 307
    A2_USER_DELETED_KEEP_DATA=Setting(
308 308
        default=['email', 'uuid', 'phone'], definition='User data to keep after deletion'
src/authentic2/utils/sms.py
29 29
    from requests.sessions import Session as Requests  # # pylint: disable=ungrouped-imports
30 30

  
31 31

  
32
def sms_ratelimit_key(group, request):
33
    if 'phone' in request.session:
34
        phone = request.session['phone']
35
        return f'{group}:{phone}'
36
    else:
37
        prefix = request.POST['phone_0'][0]
38
        number = request.POST['phone_1'][0]
39
        return f'{group}:{prefix}:{number}'
40

  
41

  
32 42
def create_sms_code():
33 43
    return ''.join(
34 44
        choices(
src/authentic2/views.py
70 70
from .utils import switch_user as utils_switch_user
71 71
from .utils.evaluate import make_condition_context
72 72
from .utils.service import get_service, set_home_url
73
from .utils.sms import SMSError, send_registration_sms
73
from .utils.sms import SMSError, send_registration_sms, sms_ratelimit_key
74 74
from .utils.view_decorators import enable_view_restriction
75 75
from .utils.views import csrf_token_check
76 76

  
......
1110 1110
            )
1111 1111
            return self.form_invalid(form)
1112 1112
        self.request.session[resend_key] = False
1113
        self.request.session['phone'] = phone
1113 1114

  
1114 1115
        if is_ratelimited(
1115 1116
            self.request,
1116
            key='post:sms',
1117
            key=sms_ratelimit_key,
1117 1118
            group='registration-sms',
1118
            rate=app_settings.A2_SMS_RATELIMIT,
1119
            rate=app_settings.A2_SMS_NUMBER_RATELIMIT,
1119 1120
            increment=True,
1120 1121
        ):
1121 1122
            form.add_error(
tests/test_views.py
16 16
# authentic2
17 17

  
18 18
import datetime
19
from random import randint
19 20
from unittest import mock
20 21
from urllib.parse import urlparse
21 22

  
22 23
import pytest
23 24
from django.urls import reverse
24 25
from django.utils.html import escape
26
from httmock import HTTMock
27
from httmock import response as httmock_response
28
from httmock import urlmatch
25 29

  
26 30
from authentic2.custom_user.models import DeletedUser, User
27 31
from authentic2.forms.passwords import PasswordChangeForm, SetPasswordForm
......
315 319
    assert response['Location'] == settings.A2_ACCOUNTS_URL
316 320

  
317 321

  
322
@pytest.mark.parametrize('view_name', ['registration_register'])  # password_lost to be added with #69890
323
def test_views_sms_ratelimit(app, db, simple_user, settings, freezer, view_name):
324
    freezer.move_to('2020-01-01')
325
    settings.A2_SMS_IP_RATELIMIT = '10/h'
326
    settings.A2_SMS_NUMBER_RATELIMIT = '3/d'
327
    settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
328
    settings.SMS_SENDER = 'EO'
329
    settings.SMS_URL = 'https://www.example.com/send'
330

  
331
    @urlmatch(scheme='https', netloc='www.example.com', path='/send')
332
    def sms_endpoint_response(url, request):
333
        return httmock_response(200, {})
334

  
335
    with HTTMock(sms_endpoint_response):
336
        # reach email limit
337
        response = app.get(reverse(view_name))
338
        response.form.set('phone_0', '33')
339
        response.form.set('phone_1', '0612345678')
340
        response = response.form.submit()
341
        assert 'try again later' not in response.text
342
        for _ in range(2):
343
            response = app.get(reverse(view_name))
344
            response.form.set('phone_0', '33')
345
            response.form.set('phone_1', '0612345678')
346
            response = response.form.submit()
347
            response = response.form.submit()  # validate warning message "sms already sent"
348
            assert 'try again later' not in response.text
349

  
350
        response = app.get(reverse(view_name))
351
        response.form.set('phone_0', '33')
352
        response.form.set('phone_1', '0612345678')
353
        response = response.form.submit().form.submit()
354
        assert 'try again later' in response.text
355

  
356
        # reach ip limit
357
        for _ in range(7):
358
            response = app.get(reverse(view_name))
359
            random_suffix = randint(0, 9999)
360
            response.form.set('phone_0', '33')
361
            response.form.set('phone_1', f'061234{random_suffix:04d}')
362
            response = response.form.submit()
363
            assert 'try again later' not in response.text
364

  
365
        response = app.get(reverse(view_name))
366
        random_suffix = randint(0, 9999)
367
        response.form.set('phone_0', '33')
368
        response.form.set('phone_1', f'061234{random_suffix:04d}')
369
        response = response.form.submit()
370
        assert 'try again later' in response.text
371

  
372
        # ip ratelimits are lifted after an hour
373
        freezer.tick(datetime.timedelta(hours=1))
374
        response = app.get(reverse(view_name))
375
        random_suffix = randint(0, 9999)
376
        response.form.set('phone_0', '33')
377
        response.form.set('phone_1', f'061234{random_suffix:04d}')
378
        response = response.form.submit()
379
        assert 'try again later' not in response.text
380

  
381
        # email ratelimits are lifted after a day
382
        response = app.get(reverse(view_name))
383
        response.form.set('phone_0', '33')
384
        response.form.set('phone_1', '0612345678')
385
        response = response.form.submit().form.submit()
386
        assert 'try again later' in response.text
387

  
388
        freezer.tick(datetime.timedelta(days=1))
389
        response = app.get(reverse(view_name))
390
        response.form.set('phone_0', '33')
391
        response.form.set('phone_1', '0612345678')
392
        response = response.form.submit()
393
        assert 'try again later' not in response.text
394

  
395

  
318 396
@pytest.mark.parametrize('view_name', ['registration_register', 'password_reset'])
319 397
def test_views_email_ratelimit(app, db, simple_user, settings, mailoutbox, freezer, view_name):
320 398
    freezer.move_to('2020-01-01')
321
-