Repository: pennersr/django-allauth

Issue:
Leaking of password reset token through the reset url
I think it is possible to leak the password reset token since it is left in the url. 

In Django 1.11 the token is stripped during a redirect ([docs](https://docs.djangoproject.com/en/1.11/releases/1.11/#django-contrib-auth), [code](https://github.com/django/django/blob/1.11.2/django/contrib/auth/views.py#L461)) to prevent the token from being taken in the referrer header from 3rd party apps on the page. I haven't dug too deeply into the source for this project but at a first glance it seems that the vulnerability exists. 

If this is the case would be happy to help fix similar to [django-registration-redux](https://github.com/macropin/django-registration/pull/268), or at the very least alert you to the issue. Let me know if you guys need any help!


Discussion:

Resolving patch:
diff --git a/allauth/account/views.py b/allauth/account/views.py
--- a/allauth/account/views.py
+++ b/allauth/account/views.py
@@ -40,6 +40,10 @@
 )
 
 
+INTERNAL_RESET_URL_KEY = "set-password"
+INTERNAL_RESET_SESSION_KEY = "_password_reset_key"
+
+
 sensitive_post_parameters_m = method_decorator(
     sensitive_post_parameters('password', 'password1', 'password2'))
 
@@ -61,6 +65,7 @@ def _ajax_response(request, response, form=None, data=None):
 
 
 class RedirectAuthenticatedUserMixin(object):
+
     def dispatch(self, request, *args, **kwargs):
         if is_authenticated(request.user) and \
                 app_settings.AUTHENTICATED_LOGIN_REDIRECTS:
@@ -671,21 +676,36 @@ def get_form_class(self):
     def dispatch(self, request, uidb36, key, **kwargs):
         self.request = request
         self.key = key
-        # (Ab)using forms here to be able to handle errors in XHR #890
-        token_form = UserTokenForm(data={'uidb36': uidb36, 'key': key})
 
-        if not token_form.is_valid():
-            self.reset_user = None
-            response = self.render_to_response(
-                self.get_context_data(token_fail=True)
-            )
-            return _ajax_response(self.request, response, form=token_form)
+        if self.key == INTERNAL_RESET_URL_KEY:
+            self.key = self.request.session.get(INTERNAL_RESET_SESSION_KEY, '')
+            # (Ab)using forms here to be able to handle errors in XHR #890
+            token_form = UserTokenForm(
+                data={'uidb36': uidb36, 'key': self.key})
+            if token_form.is_valid():
+                self.reset_user = token_form.reset_user
+                return super(PasswordResetFromKeyView, self).dispatch(request,
+                                                                      uidb36,
+                                                                      self.key,
+                                                                      **kwargs)
         else:
-            self.reset_user = token_form.reset_user
-            return super(PasswordResetFromKeyView, self).dispatch(request,
-                                                                  uidb36,
-                                                                  key,
-                                                                  **kwargs)
+            token_form = UserTokenForm(
+                data={'uidb36': uidb36, 'key': self.key})
+            if token_form.is_valid():
+                # Store the key in the session and redirect to the
+                # password reset form at a URL without the key. That
+                # avoids the possibility of leaking the key in the
+                # HTTP Referer header.
+                self.request.session[INTERNAL_RESET_SESSION_KEY] = self.key
+                redirect_url = self.request.path.replace(
+                    self.key, INTERNAL_RESET_URL_KEY)
+                return redirect(redirect_url)
+
+        self.reset_user = None
+        response = self.render_to_response(
+            self.get_context_data(token_fail=True)
+        )
+        return _ajax_response(self.request, response, form=token_form)
 
     def get_context_data(self, **kwargs):
         ret = super(PasswordResetFromKeyView, self).get_context_data(**kwargs)


Test patch:
diff --git a/allauth/account/tests.py b/allauth/account/tests.py
--- a/allauth/account/tests.py
+++ b/allauth/account/tests.py
@@ -284,6 +284,10 @@ def test_password_reset_flow(self):
         # Extract URL for `password_reset_from_key` view and access it
         url = body[body.find('/password/reset/'):].split()[0]
         resp = self.client.get(url)
+        # Follow the redirect the actual password reset page with the key
+        # hidden.
+        url = resp.url
+        resp = self.client.get(url)
         self.assertTemplateUsed(
             resp,
             'account/password_reset_from_key.%s' %
@@ -336,8 +340,11 @@ def test_password_reset_ACCOUNT_LOGIN_ON_PASSWORD_RESET(self):
         user = self._request_new_password()
         body = mail.outbox[0].body
         url = body[body.find('/password/reset/'):].split()[0]
+        resp = self.client.get(url)
+        # Follow the redirect the actual password reset page with the key
+        # hidden.
         resp = self.client.post(
-            url,
+            resp.url,
             {'password1': 'newpass123',
              'password2': 'newpass123'})
         self.assertTrue(is_authenticated(user))