Projet

Général

Profil

0003-misc-prevent-internal-URL-leak-in-browser-history-47.patch

Benjamin Dauvergne, 24 octobre 2020 10:08

Télécharger (12,3 ko)

Voir les différences:

Subject: [PATCH 3/3] misc: prevent internal URL leak in browser history
 (#47302)

 src/authentic2/middleware.py  | 17 ++---------------
 src/authentic2/urls.py        |  1 +
 src/authentic2/views.py       | 28 ++++++++++++++++++++++++++++
 tests/auth_fc/test_auth_fc.py | 33 +++++++++++++++++----------------
 tests/settings.py             |  1 +
 tests/test_all.py             |  6 +++---
 tests/test_registration.py    | 12 ++++++------
 7 files changed, 58 insertions(+), 40 deletions(-)
src/authentic2/middleware.py
26 26
from django.utils.functional import SimpleLazyObject
27 27
from django.utils.translation import ugettext as _
28 28
from django.utils.six.moves.urllib import parse as urlparse
29
from django.shortcuts import render
30 29

  
31 30
from . import app_settings, utils, plugins
32 31
from .utils.service import get_service_from_request, get_service_from_session
......
162 161
            return response
163 162
        # Check if there is some messages to show
164 163
        storage = messages.get_messages(request)
165
        if not storage:
164
        if not len(storage):
166 165
            return response
167
        only_info = True
168
        some_message = False
169
        for message in storage:
170
            some_message = True
171
            if message.level != messages.INFO:
172
                # If there are warnin or error messages, the intermediate page must not redirect
173
                # automatically but should ask for an user confirmation
174
                only_info = False
175
        storage.used = False
176
        if not some_message:
177
            return response
178
        return render(request, 'authentic2/display_message_and_continue.html',
179
                      {'url': url, 'only_info': only_info})
166
        return utils.redirect(request, 'continue', resolve=True, params={'next': url})
180 167

  
181 168

  
182 169
class ServiceAccessControlMiddleware(MiddlewareMixin):
src/authentic2/urls.py
114 114
    url(r'^idp/', include('authentic2.idp.urls')),
115 115
    url(r'^manage/', include('authentic2.manager.urls')),
116 116
    url(r'^api/', include('authentic2.api_urls')),
117
    url(r'^continue/$', views.display_message_and_continue, name='continue'),
117 118
]
118 119

  
119 120
try:
src/authentic2/views.py
1363 1363
    if message:
1364 1364
        messages.info(request, message)
1365 1365
    return utils.redirect(request, to=to)
1366

  
1367

  
1368
class DisplayMessageAndContinueView(TemplateView):
1369
    template_name = 'authentic2/display_message_and_continue.html'
1370

  
1371
    def get(self, request, *args, **kwargs):
1372
        self.url = utils.select_next_url(self.request, reverse('account_management'))
1373
        self.only_info = True
1374

  
1375
        storage = messages.get_messages(request)
1376
        if not len(storage):
1377
            return utils.redirect(request, self.url, resolve=False)
1378

  
1379
        for message in storage:
1380
            if message.level != messages.INFO:
1381
                # If there are warning or error messages, the intermediate page must not redirect
1382
                # automatically but should ask for an user confirmation
1383
                self.only_info = False
1384
        storage.used = False
1385
        return super().get(request, *args, **kwargs)
1386

  
1387
    def get_context_data(self, **kwargs):
1388
        ctx = super().get_context_data(**kwargs)
1389
        ctx['url'] = self.url
1390
        ctx['only_info'] = self.only_info
1391
        return ctx
1392

  
1393
display_message_and_continue = DisplayMessageAndContinueView.as_view()
tests/auth_fc/test_auth_fc.py
191 191
        response.form.set('new_password1', 'ikKL1234')
192 192
        response.form.set('new_password2', 'ikKL1234')
193 193
        response = response.form.submit(name='unlink')
194
        assert 'The link with the FranceConnect account has been deleted' in response.text
195 194
        assert models.FcAccount.objects.count() == 0
196
        continue_url = response.pyquery('a#a2-continue').attr['href']
195
        continue_url = response.location
197 196
        state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
198 197
        assert app.session['fc_states'][state]['next'] == '/accounts/'
199
        response = app.get(reverse('fc-logout') + '?state=' + state)
200
        assert path(response['Location']) == '/accounts/'
198
        response = app.get(reverse('fc-logout') + '?state=' + state).maybe_follow()
199
        assert response.request.path == '/accounts/'
200
        assert 'The link with the FranceConnect account has been deleted' in response.text
201 201

  
202 202

  
203 203
def test_login_email_is_unique(app, fc_settings, caplog):
......
435 435
    response.form.set('new_password1', 'ikKL1234')
436 436
    response.form.set('new_password2', 'ikKL1234')
437 437
    response = response.form.submit(name='unlink')
438
    assert 'The link with the FranceConnect account has been deleted' in response.text
439 438
    assert models.FcAccount.objects.count() == 0
440
    continue_url = response.pyquery('a#a2-continue').attr['href']
439
    continue_url = response.location
441 440
    state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
442 441
    assert app.session['fc_states'][state]['next'] == '/accounts/'
443
    response = app.get(reverse('fc-logout') + '?state=' + state)
444
    assert path(response['Location']) == '/accounts/'
442
    response = app.get(reverse('fc-logout') + '?state=' + state).maybe_follow()
443
    assert response.request.path == '/accounts/'
444
    assert 'The link with the FranceConnect account has been deleted' in response.text
445 445

  
446 446

  
447 447
def test_registration2(app, fc_settings, caplog, hooks):
......
529 529
    response.form.set('new_password1', 'ikKL1234')
530 530
    response.form.set('new_password2', 'ikKL1234')
531 531
    response = response.form.submit(name='unlink')
532
    assert 'The link with the FranceConnect account has been deleted' in response.text
533 532
    assert models.FcAccount.objects.count() == 0
534
    continue_url = response.pyquery('a#a2-continue').attr['href']
533
    continue_url = response.location
535 534
    state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
536 535
    assert app.session['fc_states'][state]['next'] == '/accounts/'
537
    response = app.get(reverse('fc-logout') + '?state=' + state)
538
    assert path(response['Location']) == '/accounts/'
536
    response = app.get(reverse('fc-logout') + '?state=' + state).maybe_follow()
537
    assert response.request.path == '/accounts/'
538
    assert 'The link with the FranceConnect account has been deleted' in response.text
539 539

  
540 540

  
541 541
def test_can_change_password(app, fc_settings, caplog, hooks):
......
653 653
    response.form.set('new_password1', 'ikKL1234')
654 654
    response.form.set('new_password2', 'ikKL1234')
655 655
    response = response.form.submit(name='unlink')
656
    continue_url = response.pyquery('a#a2-continue').attr['href']
656
    assert models.FcAccount.objects.count() == 0
657
    continue_url = response.location
657 658
    state = urlparse.parse_qs(urlparse.urlparse(continue_url).query)['state'][0]
658 659
    assert app.session['fc_states'][state]['next'] == '/accounts/'
659
    response = app.get(reverse('fc-logout') + '?state=' + state)
660
    assert path(response['Location']) == '/accounts/'
661
    response = response.follow()
660
    response = app.get(reverse('fc-logout') + '?state=' + state).maybe_follow()
661
    assert response.request.path == '/accounts/'
662
    assert 'The link with the FranceConnect account has been deleted' in response.text
662 663
    assert len(response.pyquery('[href*="password/change"]')) > 0
663 664

  
664 665

  
tests/settings.py
56 56
A2_MAX_EMAILS_FOR_ADDRESS = None
57 57

  
58 58
A2_TOKEN_EXISTS_WARNING = False
59
A2_REDIRECT_WHITELIST = ['http://sp.org/']
tests/test_all.py
497 497
        # User side
498 498
        client = Client()
499 499
        activation_url = get_link_from_mail(mail.outbox[-1])
500
        response = client.get(activation_url)
500
        response = client.get(activation_url, follow=True)
501 501
        self.assertEqual(response.status_code, status.HTTP_200_OK)
502 502
        assert utils.make_url(return_url, params={'token': token}) in force_text(response.content)
503 503
        self.assertEqual(User.objects.count(), user_count + 1)
......
611 611
        # User side - user click on email
612 612
        client = Client()
613 613
        activation_url = get_link_from_mail(mail.outbox[0])
614
        response = client.get(activation_url)
614
        response = client.get(activation_url, follow=True)
615 615
        self.assertEqual(response.status_code, status.HTTP_200_OK)
616 616
        assert utils.make_url(return_url, params={'token': token}) in force_text(response.content)
617 617
        self.assertEqual(User.objects.count(), user_count + 1)
......
717 717
        # User side - user click on first email
718 718
        client = Client()
719 719
        activation_url = get_link_from_mail(activation_mail1)
720
        response = client.get(activation_url)
720
        response = client.get(activation_url, follow=True)
721 721
        self.assertEqual(response.status_code, status.HTTP_200_OK)
722 722
        assert utils.make_url(return_url, params={'token': token}) in force_text(response.content)
723 723
        self.assertEqual(User.objects.count(), user_count + 1)
tests/test_registration.py
75 75
    # set valid password
76 76
    response.form.set('password1', 'T0==toto')
77 77
    response.form.set('password2', 'T0==toto')
78
    response = response.form.submit()
78
    response = response.form.submit().maybe_follow()
79 79
    if good_next_url:
80 80
        assert 'You have just created an account.' in response.text
81 81
        assert next_url in response.text
82
        assert response.request.path == '/continue/'
82 83
    else:
83
        assert urlparse(response['Location']).path == '/'
84
        response = response.follow()
84
        assert response.request.path == '/'
85 85
        assert 'You have just created an account.' in response.text
86 86
    assert User.objects.count() == 1
87 87
    assert len(mailoutbox) == 2
......
134 134
    response.form.set('username', 'toto')
135 135
    response.form.set('password1', 'T0==toto')
136 136
    response.form.set('password2', 'T0==toto')
137
    response = response.form.submit()
137
    response = response.form.submit().follow()
138 138
    assert 'You have just created an account.' in response.text
139 139
    assert next_url in response.text
140 140
    assert len(mailoutbox) == 2
......
572 572
    response = app.get(link)
573 573
    response.form.set('password1', 'T0==toto')
574 574
    response.form.set('password2', 'T0==toto')
575
    response = response.form.submit()
575
    response = response.form.submit().follow()
576 576

  
577 577
    assert 'You have just created an account.' in response.text
578 578
    assert new_next_url in response.text
......
618 618
    response = app.get(link)
619 619
    response.form.set('password1', 'T0==toto')
620 620
    response.form.set('password2', 'T0==toto')
621
    response = response.form.submit()
621
    response = response.form.submit().follow()
622 622
    assert new_next_url in response.text
623 623

  
624 624

  
625
-