wip/78157-phone-registration-enforce-signed-next-url (#78157) #71

Open
pmarillonnet wants to merge 2 commits from wip/78157-phone-registration-enforce-signed-next-url into main
4 changed files with 61 additions and 17 deletions

View File

@ -762,7 +762,7 @@ def get_registration_url(request):
next_url, request=request, keep_params=True, include=(constants.NONCE_FIELD_NAME,), resolve=False
)
params = {REDIRECT_FIELD_NAME: next_url}
return make_url('registration_register', params=params)
return make_url('registration_register', params=params, next_url=next_url, sign_next_url=True)
def get_token_login_url(user):
@ -1023,21 +1023,23 @@ def get_good_origins():
return list(urls)
def good_next_url(request, next_url):
def good_next_url(request, next_url, signature_required=False):
'''Check if an URL is a good next_url'''
if not next_url:
return False
signature = request.POST.get(constants.NEXT_URL_SIGNATURE) or request.GET.get(
constants.NEXT_URL_SIGNATURE
)
if signature_required and not signature:
return False
if signature:
# the signed-next origin is considered trusted, a valid signature bypasses origin checks
return crypto.check_hmac_url(settings.SECRET_KEY, next_url, signature)
if next_url.startswith('/') and (len(next_url) == 1 or next_url[1] != '/'):
return True
if same_origin(request.build_absolute_uri(), next_url):
return True
signature = request.POST.get(constants.NEXT_URL_SIGNATURE) or request.GET.get(
constants.NEXT_URL_SIGNATURE
)
if signature:
return crypto.check_hmac_url(settings.SECRET_KEY, next_url, signature)
for origin in get_good_origins():
if same_origin(next_url, origin):
return True

View File

@ -1122,7 +1122,6 @@ class BaseRegistrationView(HomeURLMixin, FormView):
self.token = {}
self.ou = get_default_ou()
self.next_url = utils_misc.select_next_url(request, None)
# load pre-filled values when registering with email address
if request.GET.get('token'):
try:
@ -1134,12 +1133,7 @@ class BaseRegistrationView(HomeURLMixin, FormView):
return HttpResponseBadRequest('invalid token', content_type='text/plain')
if 'ou' in self.token:
self.ou = OU.objects.get(pk=self.token['ou'])
if self.token.get(REDIRECT_FIELD_NAME):
self.next_url = self.token.pop(REDIRECT_FIELD_NAME)
elif (next_url := request.GET.get(REDIRECT_FIELD_NAME)) and utils_misc.good_next_url(
request, next_url
):
self.next_url = next_url
self.next_url = self.token.pop(REDIRECT_FIELD_NAME, utils_misc.select_next_url(request, None))
set_home_url(request, self.next_url)
return super().dispatch(request, *args, **kwargs)
@ -1159,6 +1153,14 @@ class BaseRegistrationView(HomeURLMixin, FormView):
if app_settings.A2_ACCEPT_PHONE_AUTHENTICATION:
phone = form.cleaned_data.pop('phone')
if phone:
# enforce signed next_urls (only phone-based registration supported yet)
if (next_url := self.request.GET.get(REDIRECT_FIELD_NAME)) and utils_misc.good_next_url(
self.request, next_url, signature_required=True
):
self.next_url = next_url
else:
self.next_url = '/'
return self.perform_phone_registration(form, phone)
return ValidationError(_('No means of registration provided.'))

View File

@ -215,7 +215,7 @@ def test_show_condition_with_headers(db, app, settings):
def test_registration_url_on_login_page(db, app):
response = app.get('/login/?next=/whatever')
assert 'register/?next=/whatever"' in response
assert 'register/?next=/whatever&next-signature=' in response
def test_redirect_login_to_homepage(db, app, settings, simple_user, superuser):

View File

@ -1110,3 +1110,43 @@ def test_phone_registration_redirect_url(app, db, settings):
resp.follow()
user = User.objects.get(first_name='John', last_name='Doe')
assert user.phone == '+33612345678'
def test_phone_registration_redirect_url_no_signature_is_ignored(app, db, settings):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
resp = app.get(reverse('registration_register') + '?next=/accounts/consents/')
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
resp.form.set('password1', 'Password0')
resp.form.set('password2', 'Password0')
resp.form.set('first_name', 'John')
resp.form.set('last_name', 'Doe')
resp = resp.form.submit()
assert resp.location == '/'
def test_phone_registration_redirect_url_forged_signature_is_ignored(app, db, settings):
settings.A2_ACCEPT_PHONE_AUTHENTICATION = True
settings.SMS_URL = 'https://foo.whatever.none/'
resp = app.get(reverse('registration_register') + '?next=/accounts/consents/&next-signature=abc')
resp.form.set('phone_1', '612345678')
with HTTMock(sms_service_mock):
resp = resp.form.submit().follow()
code = SMSCode.objects.get()
resp.form.set('sms_code', code.value)
resp = resp.form.submit().follow()
resp.form.set('password1', 'Password0')
resp.form.set('password2', 'Password0')
resp.form.set('first_name', 'John')
resp.form.set('last_name', 'Doe')
resp = resp.form.submit()
assert resp.location == '/'