# HG changeset patch # User Mads Kiilerich # Date 2020-01-09 12:28:33 # Node ID 8b47181750a82188369cf013fc8b30c321a391dd # Parent bbbfee57fb963779f3f80ad9ad5e0ec28154b8f0 login: fix incorrect CSRF rejection of "Reset Your Password" form (Issue #350) htmlfill would remove the CSRF token from the form when substituting the query parameters, causing password reset to break. By default, htmlfill will clear all input fields that doesn't have a new "default" value provided. It could be fixed by setting force_defaults to False - see http://www.formencode.org/en/1.2-branch/modules/htmlfill.html . It could also be fixed by providing the CSRF token in the defaults to be substituted in the form. Instead, refactor password_reset_confirmation to have more explicitly safe handling of query parameters. Replace htmlfill with the usual template variables. The URLs are generated in kallithea/model/user.py send_reset_password_email() and should only contain email, timestamp (integer as digit string) and a hex token from get_reset_password_token() . diff --git a/kallithea/controllers/login.py b/kallithea/controllers/login.py --- a/kallithea/controllers/login.py +++ b/kallithea/controllers/login.py @@ -210,12 +210,10 @@ class LoginController(BaseController): # The template needs the email address outside of the form. c.email = request.params.get('email') - + c.timestamp = request.params.get('timestamp') or '' + c.token = request.params.get('token') or '' if not request.POST: - return htmlfill.render( - render('/password_reset_confirmation.html'), - defaults=dict(request.params), - encoding='UTF-8') + return render('/password_reset_confirmation.html') form = PasswordResetConfirmationForm()() try: diff --git a/kallithea/templates/password_reset_confirmation.html b/kallithea/templates/password_reset_confirmation.html --- a/kallithea/templates/password_reset_confirmation.html +++ b/kallithea/templates/password_reset_confirmation.html @@ -22,13 +22,13 @@ ${h.form(h.url('reset_password_confirmation'), method='post')}

${_('You are about to set a new password for the email address %s.') % c.email}

${_('Note that you must use the same browser session for this as the one used to request the password reset.')}

- ${h.hidden('email')} - ${h.hidden('timestamp')} + ${h.hidden('email', value=c.email)} + ${h.hidden('timestamp', value=c.timestamp)}
- ${h.text('token', class_='form-control')} + ${h.text('token', value=c.token, class_='form-control')}
diff --git a/kallithea/tests/functional/test_login.py b/kallithea/tests/functional/test_login.py --- a/kallithea/tests/functional/test_login.py +++ b/kallithea/tests/functional/test_login.py @@ -459,12 +459,12 @@ class TestLoginController(TestController assert response.status == '200 OK' response.mustcontain("You are about to set a new password for the email address %s" % email) response.mustcontain('
' % url(controller='login', action='password_reset_confirmation')) - response.mustcontain(no='value="%s"' % self.session_csrf_secret_token()) # BUG making actual password_reset_confirmation POST fail + response.mustcontain('value="%s"' % self.session_csrf_secret_token()) response.mustcontain('value="%s"' % token) response.mustcontain('value="%s"' % timestamp) response.mustcontain('value="username@example.com"') - # fake a submit of that form (*with* csrf token) + # fake a submit of that form response = self.app.post(url(controller='login', action='password_reset_confirmation'), {'email': email,