Projet

Général

Profil

0001-whitelist-send_registration_email_next_url-using-HMA.patch

Benjamin Dauvergne, 19 juin 2019 15:50

Télécharger (6,56 ko)

Voir les différences:

Subject: [PATCH] whitelist send_registration_email_next_url using HMAC
 signature (#34115)

 src/authentic2/constants.py |  1 +
 src/authentic2/crypto.py    | 17 +++++++++++++++++
 src/authentic2/utils.py     | 18 ++++++++++++++----
 src/authentic2/views.py     |  2 +-
 tests/test_api.py           |  4 +++-
 5 files changed, 36 insertions(+), 6 deletions(-)
src/authentic2/constants.py
21 21
SWITCH_USER_SESSION_KEY = '_switch_user'
22 22
LAST_LOGIN_SESSION_KEY = '_last_login'
23 23
SERVICE_FIELD_NAME = 'service'
24
NEXT_URL_SIGNATURE = 'next-signature'
src/authentic2/crypto.py
16 16

  
17 17
import base64
18 18
import hashlib
19
import hmac
19 20
import struct
20 21

  
21 22
from Crypto.Cipher import AES
......
24 25
from Crypto.Hash import HMAC
25 26
from Crypto import Random
26 27

  
28
from django.utils.crypto import constant_time_compare
29

  
27 30

  
28 31
class DecryptionError(Exception):
29 32
    pass
......
177 180
        if not raise_on_error:
178 181
            return None
179 182
        raise
183

  
184

  
185
def hmac_url(key, url):
186
    if hasattr(key, 'encode'):
187
        key = key.encode('utf-8')
188
    if hasattr(url, 'decode'):
189
        url = url.encode('ascii')
190
    return base64.b32encode(hmac.HMAC(key=key, msg=url, digestmod=hashlib.sha256).digest()).strip('=')
191

  
192

  
193
def check_hmac_url(key, url, signature):
194
    if hasattr(signature, 'decode'):
195
        signature = signature.decode('ascii')
196
    return constant_time_compare(signature, hmac_url(key, url))
src/authentic2/utils.py
62 62
from authentic2.saml.saml2utils import filter_attribute_private_key, \
63 63
    filter_element_private_key
64 64

  
65
from . import plugins, app_settings, constants
65
from . import plugins, app_settings, constants, crypto
66 66

  
67 67

  
68 68
class CleanLogMessage(logging.Filter):
......
731 731
    logger.info(u'account deletion mail sent to %s', user.email)
732 732

  
733 733

  
734
def build_reset_password_url(user, request=None, next_url=None, set_random_password=True):
734
def build_reset_password_url(user, request=None, next_url=None, set_random_password=True, sign_next_url=True):
735 735
    '''Build a reset password URL'''
736 736
    from .compat import default_token_generator
737 737

  
......
744 744
    if request:
745 745
        reset_url = request.build_absolute_uri(reset_url)
746 746
    if next_url:
747
        reset_url += '?' + urlparse.urlencode({'next': next_url})
747
        params = {'next': next_url}
748
        if sign_next_url:
749
            params[constants.NEXT_URL_SIGNATURE] = crypto.hmac_url(settings.SECRET_KEY, next_url)
750
        reset_url += '?' + urlparse.urlencode(params)
748 751
    return reset_url, token
749 752

  
750 753

  
......
754 757
                             legacy_subject_templates=['registration/password_reset_subject.txt'],
755 758
                             legacy_body_templates=['registration/password_reset_email.html'],
756 759
                             set_random_password=True,
760
                             sign_next_url=True,
757 761
                             **kwargs):
758 762
    from . import middleware
759 763

  
......
775 779

  
776 780
    # Build reset URL
777 781
    ctx['reset_url'], token = build_reset_password_url(user, request=request, next_url=next_url,
778
                                                       set_random_password=set_random_password)
782
                                                       set_random_password=set_random_password,
783
                                                       sign_next_url=sign_next_url)
779 784

  
780 785
    send_templated_mail(user.email, template_names, ctx, request=request,
781 786
                        legacy_subject_templates=legacy_subject_templates,
......
894 899
        return True
895 900
    if same_origin(request.build_absolute_uri(), next_url):
896 901
        return True
902
    signature = request.POST.get(constants.NEXT_URL_SIGNATURE) or request.GET.get(constants.NEXT_URL_SIGNATURE)
903

  
904
    if signature:
905
        return crypto.check_hmac_url(settings.SECRET_KEY, next_url, signature)
906

  
897 907
    for origin in app_settings.A2_REDIRECT_WHITELIST:
898 908
        if same_origin(next_url, origin):
899 909
            return True
src/authentic2/views.py
644 644
    def get_form_kwargs(self, **kwargs):
645 645
        kwargs = super(PasswordResetView, self).get_form_kwargs(**kwargs)
646 646
        initial = kwargs.setdefault('initial', {})
647
        initial['next_url'] = self.request.GET.get(REDIRECT_FIELD_NAME, '')
647
        initial['next_url'] = utils.select_next_url(self.request, '')
648 648
        return kwargs
649 649

  
650 650
    def get_context_data(self, **kwargs):
tests/test_api.py
575 575
        'email': 'john.doe@example.net',
576 576
        'title': 'Mr',
577 577
        'send_registration_email': True,
578
        'send_registration_email_next_url': 'http://example.com/',
578 579
    }
579 580
    assert len(mail.outbox) == 0
580 581
    resp = app.post_json('/api/users/', params=payload, status=201)
......
586 587
    resp = app.get(relative_url, status=200)
587 588
    resp.form.set('new_password1', '1234==aA')
588 589
    resp.form.set('new_password2', '1234==aA')
589
    resp = resp.form.submit().follow()
590
    resp = resp.form.submit()
590 591
    # Check user was properly logged in
591 592
    assert str(app.session['_auth_user_id']) == str(user_id)
593
    assert resp.location == 'http://example.com/'
592 594

  
593 595

  
594 596
def test_api_users_create_force_password_reset(app, client, settings, superuser):
595
-