[PATCH PoC] secure password reset implementation

Andrew Shadura andrew at shadura.me
Wed May 20 04:38:13 EDT 2015


Hello,

On Tue, 19 May 2015 17:10:46 +0200
Mads Kiilerich <mads at kiilerich.com> wrote:

> > The idea behind is to generate a token which is dependent on the
> > user state at the time before the password change takes place,
> > so the token is one-time and can't be reused, and also to bind the
> > token the the session.
> >
> > The token is calculated as SHA1 hash of the following:
> >
> >      * user's identifier (number, not a name)
> >      * timestamp
> >      * hashed user's password
> >      * session identifier
> >      * per-application secret
> >
> > We use numeric user's identifier, as it's fixed and doesn't change,
> > so renaming users doesn't affect the mechanism. Timestamp is added
> > to make it possible to limit the token's validness (currently not
> > implemented),

> Please just hardcode something like 24 hours.

Okay, I will.

> > and we don't want users to be able to fake that field
> > easily. Hashed user's password is needed to prevent using the token
> > again once the password has been changed. Session identifier is
> > an additional security measure to ensure someone else stealing the
> > token can't use it. Finally, per-application secret is just another
> > way to make it harder for an attacker to guess all values in an
> > attempt to generate a valid token.

> Are you or others aware of "standard" ways of handling password
> resets? Something that indicates we are doing it correctly or that we
> can reuse?

I looked at what other frameworks do, and from what I saw it seems they
do more or less the same thing.

> >       def password_reset_confirmation(self):
> > -        if request.GET and request.GET.get('key'):
> > +        if request.GET:
> > +            c.data = dict(
> > +                username = request.GET.get('username'),
> > +                timestamp = request.GET.get('timestamp'),
> > +                token = request.GET.get('token')
> > +            )
> > +            if c.data['token']:
> > +                try:
> > +                    log.debug("data = %s" % c.data)
> > +                    if UserModel().reset_password_confirm(c.data):
> > +                        return
> > render('/password_reset_confirmation.html')
> > +                    else:
> > +                        h.flash(_('Invalid password reset token'),
> > +                                category='error')
> > +                        return redirect(url('reset_password'))
> > +                except Exception as e:
> > +                    log.error(e)
> > +                    return redirect(url('reset_password'))

> I don't like the pattern of redirecting on serious internal errors
> and have been trying to get rid of it in other places. I think it
> generally is much better to just show an error message so the error
> can be fixed ... and the user easily can use back to go back and
> retry.

Okay, I will fail with 500 or a better code (if applicable) here.

> > @@ -296,10 +300,19 @@ class UserModel(BaseModel):
> >   
> >           user_email = data['email']
> >           user = User.get_by_email(user_email)
> > +        timestamp = int(time.time())
> >           if user:
> >               log.debug('password reset user found %s' % user)
> > +            token = hashlib.sha1('%s%s%s%s%s' % (user.user_id,
> > +                                               timestamp,
> > +                                               user.password,
> > +                                               session.id,
> > +
> > config.get('app_instance_uuid'))
> > +            ).hexdigest()
> 
> Now we are reinventing crypto. That is dangerous. One thing: One
> thing is that hashes could give conflicts in the hash but we should
> make sure that we never got "collisions" in the input string - such
> collisions could perhaps be used as attacks. Perhaps use \n as
> delimiter of the fields?

Well, none of the data we use here is accepted directly from user
(except timestamp — when verifying the token; I should probably
validate it before using). From what I saw in similar cases elsewhere
(for example, OAuth 1.0), sometimes the data is escaped to avoid
collisions.

> The computation should probably be moved to a function so it can be
> reused.

Agree.

> > +    </div>
> > +    <div class="inner">
> > +        %if c.data['username']:
> > +        <p>${_('You are about to reset password for the user %s')
> > % c.data['username']}</p>

> Reset _the_ password

> We should however not reveal the username - at most the email address 
> the user provided. We should also make sure the same message is
> shown, no matter if the address is recognized or not.

Well, yes, you're right. I shouldn't do email → username conversion.

> But there is no information about any code in the email - there is
> only a URL?

> We should either explicitly show the code in the mail or just ask the 
> user to click the link in the mail and not show the "code" input
> field.

The template isn't perfect, that's the problem :) The idea that you
receive an email with a link (with token) and a token separately from a
link, so you can choose to either follow a link, or to type a code
manually. The password reset form redirects you to a form where the
only thing missing is the token.

I'll post a revised version of a patch soon.

-- 
Cheers,
  Andrew
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 836 bytes
Desc: OpenPGP digital signature
URL: <http://lists.sfconservancy.org/pipermail/kallithea-general/attachments/20150520/fe1ba48f/attachment-0001.sig>


More information about the kallithea-general mailing list