Răsfoiți Sursa

Fixed #26956 -- Added success_url_allowed_hosts to LoginView and LogoutView.

Allows specifying additional hosts to redirect after login and log out.
Jon Dufresne 8 ani în urmă
părinte
comite
66e1ebbffc

+ 15 - 5
django/contrib/auth/views.py

@@ -53,7 +53,16 @@ def deprecate_current_app(func):
     return inner
 
 
-class LoginView(FormView):
+class SuccessURLAllowedHostsMixin(object):
+    success_url_allowed_hosts = set()
+
+    def get_success_url_allowed_hosts(self):
+        allowed_hosts = {self.request.get_host()}
+        allowed_hosts.update(self.success_url_allowed_hosts)
+        return allowed_hosts
+
+
+class LoginView(SuccessURLAllowedHostsMixin, FormView):
     """
     Displays the login form and handles the login action.
     """
@@ -86,7 +95,7 @@ class LoginView(FormView):
         )
         url_is_safe = is_safe_url(
             url=redirect_to,
-            allowed_hosts={self.request.get_host()},
+            allowed_hosts=self.get_success_url_allowed_hosts(),
             require_https=self.request.is_secure(),
         )
         if not url_is_safe:
@@ -123,7 +132,7 @@ def login(request, *args, **kwargs):
     return LoginView.as_view(**kwargs)(request, *args, **kwargs)
 
 
-class LogoutView(TemplateView):
+class LogoutView(SuccessURLAllowedHostsMixin, TemplateView):
     """
     Logs out the user and displays 'You are logged out' message.
     """
@@ -157,10 +166,11 @@ class LogoutView(TemplateView):
             )
             url_is_safe = is_safe_url(
                 url=next_page,
-                allowed_hosts={self.request.get_host()},
+                allowed_hosts=self.get_success_url_allowed_hosts(),
                 require_https=self.request.is_secure(),
             )
-            # Security check -- don't allow redirection to a different host.
+            # Security check -- Ensure the user-originating redirection URL is
+            # safe.
             if not url_is_safe:
                 next_page = self.request.path
         return next_page

+ 5 - 0
docs/releases/1.11.txt

@@ -101,6 +101,11 @@ Minor features
 * :func:`~django.contrib.auth.update_session_auth_hash` now rotates the session
   key to allow a password change to invalidate stolen session cookies.
 
+* The new ``success_url_allowed_hosts`` attribute for
+  :class:`~django.contrib.auth.views.LoginView` and
+  :class:`~django.contrib.auth.views.LogoutView` allows specifying a set of
+  hosts that are safe for redirecting after login and logout.
+
 :mod:`django.contrib.contenttypes`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 8 - 0
docs/topics/auth/default.txt

@@ -999,6 +999,10 @@ implementation details see :ref:`using-the-views`.
       authenticated users accessing the login page will be redirected as if
       they had just successfully logged in. Defaults to ``False``.
 
+    * ``success_url_allowed_hosts``: A :class:`set` of hosts, in addition to
+      :meth:`request.get_host() <django.http.HttpRequest.get_host>`, that are
+      safe for redirecting after login. Defaults to an empty :class:`set`.
+
     Here's what ``LoginView`` does:
 
     * If called via ``GET``, it displays a login form that POSTs to the
@@ -1138,6 +1142,10 @@ implementation details see :ref:`using-the-views`.
     * ``extra_context``: A dictionary of context data that will be added to the
       default context data passed to the template.
 
+    * ``success_url_allowed_hosts``: A :class:`set` of hosts, in addition to
+      :meth:`request.get_host() <django.http.HttpRequest.get_host>`, that are
+      safe for redirecting after logout. Defaults to an empty :class:`set`.
+
     **Template context:**
 
     * ``title``: The string "Logged out", localized.

+ 53 - 0
tests/auth_tests/test_views.py

@@ -822,6 +822,38 @@ class LoginRedirectAuthenticatedUser(AuthViewsTestCase):
                 self.client.get(url)
 
 
+class LoginSuccessURLAllowedHostsTest(AuthViewsTestCase):
+    def test_success_url_allowed_hosts_same_host(self):
+        response = self.client.post('/login/allowed_hosts/', {
+            'username': 'testclient',
+            'password': 'password',
+            'next': 'https://testserver/home',
+        })
+        self.assertIn(SESSION_KEY, self.client.session)
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, 'https://testserver/home')
+
+    def test_success_url_allowed_hosts_safe_host(self):
+        response = self.client.post('/login/allowed_hosts/', {
+            'username': 'testclient',
+            'password': 'password',
+            'next': 'https://otherserver/home',
+        })
+        self.assertIn(SESSION_KEY, self.client.session)
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, 'https://otherserver/home')
+
+    def test_success_url_allowed_hosts_unsafe_host(self):
+        response = self.client.post('/login/allowed_hosts/', {
+            'username': 'testclient',
+            'password': 'password',
+            'next': 'https://evil/home',
+        })
+        self.assertIn(SESSION_KEY, self.client.session)
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, '/accounts/profile/')
+
+
 class LogoutTest(AuthViewsTestCase):
 
     def confirm_logged_out(self):
@@ -893,6 +925,27 @@ class LogoutTest(AuthViewsTestCase):
         self.assertURLEqual(response.url, '/password_reset/')
         self.confirm_logged_out()
 
+    def test_success_url_allowed_hosts_same_host(self):
+        self.login()
+        response = self.client.get('/logout/allowed_hosts/?next=https://testserver/')
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, 'https://testserver/')
+        self.confirm_logged_out()
+
+    def test_success_url_allowed_hosts_safe_host(self):
+        self.login()
+        response = self.client.get('/logout/allowed_hosts/?next=https://otherserver/')
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, 'https://otherserver/')
+        self.confirm_logged_out()
+
+    def test_success_url_allowed_hosts_unsafe_host(self):
+        self.login()
+        response = self.client.get('/logout/allowed_hosts/?next=https://evil/')
+        self.assertEqual(response.status_code, 302)
+        self.assertURLEqual(response.url, '/logout/allowed_hosts/')
+        self.confirm_logged_out()
+
     def test_security_check(self):
         logout_url = reverse('logout')
 

+ 3 - 0
tests/auth_tests/urls.py

@@ -67,6 +67,7 @@ urlpatterns = auth_urlpatterns + [
     url(r'^logout/custom_query/$', views.LogoutView.as_view(redirect_field_name='follow')),
     url(r'^logout/next_page/$', views.LogoutView.as_view(next_page='/somewhere/')),
     url(r'^logout/next_page/named/$', views.LogoutView.as_view(next_page='password_reset')),
+    url(r'^logout/allowed_hosts/$', views.LogoutView.as_view(success_url_allowed_hosts={'otherserver'})),
     url(r'^remote_user/$', remote_user_auth_view),
 
     url(r'^password_reset_from_email/$',
@@ -106,6 +107,8 @@ urlpatterns = auth_urlpatterns + [
     url(r'^login/redirect_authenticated_user_default/$', views.LoginView.as_view()),
     url(r'^login/redirect_authenticated_user/$',
         views.LoginView.as_view(redirect_authenticated_user=True)),
+    url(r'^login/allowed_hosts/$',
+        views.LoginView.as_view(success_url_allowed_hosts={'otherserver'})),
 
     # This line is only required to render the password reset with is_admin=True
     url(r'^admin/', admin.site.urls),