Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11223 -- Fixed logout view to use the 'next' GET parameter cor…

…rectly as described in the docs, while only allowing redirection to the same host.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15706 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 751888ece3c970e208d9d77f21a35464c9122835 1 parent f6c9916
Jannis Leidel authored March 02, 2011
55  django/contrib/auth/tests/views.py
@@ -333,6 +333,19 @@ def test_14377(self):
333 333
         response = self.client.get('/logout/')
334 334
         self.assertTrue('site' in response.context)
335 335
 
  336
+    def test_logout_with_overridden_redirect_url(self):
  337
+        # Bug 11223
  338
+        self.login()
  339
+        response = self.client.get('/logout/next_page/')
  340
+        self.assertEqual(response.status_code, 302)
  341
+        self.assert_(response['Location'].endswith('/somewhere/'))
  342
+
  343
+        response = self.client.get('/logout/next_page/?next=/login/')
  344
+        self.assertEqual(response.status_code, 302)
  345
+        self.assert_(response['Location'].endswith('/login/'))
  346
+
  347
+        self.confirm_logged_out()
  348
+
336 349
     def test_logout_with_next_page_specified(self): 
337 350
         "Logout with next_page option given redirects to specified resource"
338 351
         self.login()
@@ -356,3 +369,45 @@ def test_logout_with_custom_redirect_argument(self):
356 369
         self.assertEqual(response.status_code, 302)
357 370
         self.assert_(response['Location'].endswith('/somewhere/'))
358 371
         self.confirm_logged_out()
  372
+
  373
+    def test_security_check(self, password='password'):
  374
+        logout_url = reverse('django.contrib.auth.views.logout')
  375
+
  376
+        # Those URLs should not pass the security check
  377
+        for bad_url in ('http://example.com',
  378
+                        'https://example.com',
  379
+                        'ftp://exampel.com',
  380
+                        '//example.com'
  381
+                        ):
  382
+            nasty_url = '%(url)s?%(next)s=%(bad_url)s' % {
  383
+                'url': logout_url,
  384
+                'next': REDIRECT_FIELD_NAME,
  385
+                'bad_url': urllib.quote(bad_url)
  386
+            }
  387
+            self.login()
  388
+            response = self.client.get(nasty_url)
  389
+            self.assertEquals(response.status_code, 302)
  390
+            self.assertFalse(bad_url in response['Location'],
  391
+                             "%s should be blocked" % bad_url)
  392
+            self.confirm_logged_out()
  393
+
  394
+        # These URLs *should* still pass the security check
  395
+        for good_url in ('/view/?param=http://example.com',
  396
+                         '/view/?param=https://example.com',
  397
+                         '/view?param=ftp://exampel.com',
  398
+                         'view/?param=//example.com',
  399
+                         'https:///',
  400
+                         '//testserver/',
  401
+                         '/url%20with%20spaces/', # see ticket #12534
  402
+                         ):
  403
+            safe_url = '%(url)s?%(next)s=%(good_url)s' % {
  404
+                'url': logout_url,
  405
+                'next': REDIRECT_FIELD_NAME,
  406
+                'good_url': urllib.quote(good_url)
  407
+            }
  408
+            self.login()
  409
+            response = self.client.get(safe_url)
  410
+            self.assertEquals(response.status_code, 302)
  411
+            self.assertTrue(good_url in response['Location'],
  412
+                            "%s should be allowed" % good_url)
  413
+            self.confirm_logged_out()
75  django/contrib/auth/views.py
... ...
@@ -1,23 +1,23 @@
1  
-import re
2 1
 import urlparse
  2
+
3 3
 from django.conf import settings
4  
-from django.contrib.auth import REDIRECT_FIELD_NAME
5  
-# Avoid shadowing the login() view below.
6  
-from django.contrib.auth import login as auth_login
7  
-from django.contrib.auth.decorators import login_required
8  
-from django.contrib.auth.forms import AuthenticationForm
9  
-from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
10  
-from django.contrib.auth.tokens import default_token_generator
11  
-from django.views.decorators.csrf import csrf_protect
12 4
 from django.core.urlresolvers import reverse
13  
-from django.shortcuts import render_to_response, get_object_or_404
14  
-from django.contrib.sites.models import get_current_site
15  
-from django.http import HttpResponseRedirect, Http404, QueryDict
  5
+from django.http import HttpResponseRedirect, QueryDict
  6
+from django.shortcuts import render_to_response
16 7
 from django.template import RequestContext
17 8
 from django.utils.http import base36_to_int
18 9
 from django.utils.translation import ugettext as _
19  
-from django.contrib.auth.models import User
20 10
 from django.views.decorators.cache import never_cache
  11
+from django.views.decorators.csrf import csrf_protect
  12
+
  13
+# Avoid shadowing the login() and logout() views below.
  14
+from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout
  15
+from django.contrib.auth.decorators import login_required
  16
+from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm
  17
+from django.contrib.auth.models import User
  18
+from django.contrib.auth.tokens import default_token_generator
  19
+from django.contrib.sites.models import get_current_site
  20
+
21 21
 
22 22
 @csrf_protect
23 23
 @never_cache
@@ -25,8 +25,9 @@ def login(request, template_name='registration/login.html',
25 25
           redirect_field_name=REDIRECT_FIELD_NAME,
26 26
           authentication_form=AuthenticationForm,
27 27
           current_app=None, extra_context=None):
28  
-    """Displays the login form and handles the login action."""
29  
-
  28
+    """
  29
+    Displays the login form and handles the login action.
  30
+    """
30 31
     redirect_to = request.REQUEST.get(redirect_field_name, '')
31 32
 
32 33
     if request.method == "POST":
@@ -71,36 +72,44 @@ def logout(request, next_page=None,
71 72
            template_name='registration/logged_out.html',
72 73
            redirect_field_name=REDIRECT_FIELD_NAME,
73 74
            current_app=None, extra_context=None):
74  
-    "Logs out the user and displays 'You are logged out' message."
75  
-    from django.contrib.auth import logout
76  
-    logout(request)
77  
-    if next_page is None:
78  
-        redirect_to = request.REQUEST.get(redirect_field_name, '')
79  
-        if redirect_to:
  75
+    """
  76
+    Logs out the user and displays 'You are logged out' message.
  77
+    """
  78
+    auth_logout(request)
  79
+    redirect_to = request.REQUEST.get(redirect_field_name, '')
  80
+    if redirect_to:
  81
+        netloc = urlparse.urlparse(redirect_to)[1]
  82
+        # Security check -- don't allow redirection to a different host.
  83
+        if not (netloc and netloc != request.get_host()):
80 84
             return HttpResponseRedirect(redirect_to)
81  
-        else:
82  
-            current_site = get_current_site(request)
83  
-            context = {
84  
-                'site': current_site,
85  
-                'site_name': current_site.name,
86  
-                'title': _('Logged out')
87  
-            }
88  
-            context.update(extra_context or {})
89  
-            return render_to_response(template_name, context,
90  
-                                      context_instance=RequestContext(request, current_app=current_app))
  85
+
  86
+    if next_page is None:
  87
+        current_site = get_current_site(request)
  88
+        context = {
  89
+            'site': current_site,
  90
+            'site_name': current_site.name,
  91
+            'title': _('Logged out')
  92
+        }
  93
+        context.update(extra_context or {})
  94
+        return render_to_response(template_name, context,
  95
+                                  context_instance=RequestContext(request, current_app=current_app))
91 96
     else:
92 97
         # Redirect to this page until the session has been cleared.
93 98
         return HttpResponseRedirect(next_page or request.path)
94 99
 
95 100
 def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
96  
-    "Logs out the user if he is logged in. Then redirects to the log-in page."
  101
+    """
  102
+    Logs out the user if he is logged in. Then redirects to the log-in page.
  103
+    """
97 104
     if not login_url:
98 105
         login_url = settings.LOGIN_URL
99 106
     return logout(request, login_url, current_app=current_app, extra_context=extra_context)
100 107
 
101 108
 def redirect_to_login(next, login_url=None,
102 109
                       redirect_field_name=REDIRECT_FIELD_NAME):
103  
-    "Redirects the user to the login page, passing the given 'next' page"
  110
+    """
  111
+    Redirects the user to the login page, passing the given 'next' page
  112
+    """
104 113
     if not login_url:
105 114
         login_url = settings.LOGIN_URL
106 115
 

0 notes on commit 751888e

Please sign in to comment.
Something went wrong with that request. Please try again.