Currently password reset is done without any confirmation, so all you have to do is know someone's email and a Django site that they use (assuming it uses the default password reset code) and you can change their password. In this way, you only have to make about 1 request/minute to completely block someone from accessing their account.
I propose to change to a solution that requires clicking a link in an email, with the link containing the username, the new password, a timestamp and a hash to stop tampering. This link is handled by a new view which does the resetting, and gives a limited period for the reset, so that someone who sniffs the URL cannot keep resetting the password.
As I understand it, with SSL both GET and POST parameters in a request are invisible to sniffers, so if SSL is enabled this would become a secure solution (without SSL, GET and POST etc are of course completely visible to sniffers, so you can't design a system that is properly secure without SSL).
This would be a backwards incompatible change -- if you have provided your own templates for the password reset views then they will need fixing. It doesn't make sense to do it to trunk, since the password reset view has already changed in newforms-admin, so this should probably wait for the nfa merge.
I've actually already implemented the above system for my own site, complete with tests. Testing is still problematic for views in contrib, but that should be fixed shortly.
What do people think? Did I miss any problems?
Luke
-- "If your parents never had children, the chances are you won't either."
It sounds like what you are advocating is changing the password reset
to work similar to the way activation works in James Bennett's django-
registration, is that correct?
On Jun 27, 4:01 pm, Luke Plant <L.Plant...@cantab.net> wrote:
> Currently password reset is done without any confirmation, so all you
> have to do is know someone's email and a Django site that they use
> (assuming it uses the default password reset code) and you can change
> their password. In this way, you only have to make about 1
> request/minute to completely block someone from accessing their
> account.
> I propose to change to a solution that requires clicking a link in an
> email, with the link containing the username, the new password, a
> timestamp and a hash to stop tampering. This link is handled by a
> new view which does the resetting, and gives a limited period for the
> reset, so that someone who sniffs the URL cannot keep resetting the
> password.
> As I understand it, with SSL both GET and POST parameters in a request
> are invisible to sniffers, so if SSL is enabled this would become a
> secure solution (without SSL, GET and POST etc are of course
> completely visible to sniffers, so you can't design a system that is
> properly secure without SSL).
> This would be a backwards incompatible change -- if you have provided
> your own templates for the password reset views then they will need
> fixing. It doesn't make sense to do it to trunk, since the password
> reset view has already changed in newforms-admin, so this should
> probably wait for the nfa merge.
> I've actually already implemented the above system for my own site,
> complete with tests. Testing is still problematic for views in
> contrib, but that should be fixed shortly.
> What do people think? Did I miss any problems?
> Luke
> --
> "If your parents never had children, the chances are you won't
> either."
> It sounds like what you are advocating is changing the password reset > to work similar to the way activation works in James Bennett's django- > registration, is that correct?
Similar to that is what it sounds like - I'm definitely +1 on this - a lot of sites do it like this where you must confirm a password change to prevent this kind of thing - they could initiate as many requests as they wanted without actually changing anything.
I'd suggest making the code to change the password a one-use-only item though, so that even if someone did sniff the code, it'd be useless after that.
-- Collin Grady
Abstainer, n.: A weak person who yields to the temptation of denying himself a pleasure. -- Ambrose Bierce, "The Devil's Dictionary"
On Friday 27 June 2008 23:06:57 Collin Grady wrote:
> alex.gay...@gmail.com said the following: > I'd suggest making the code to change the password a one-use-only > item though, so that even if someone did sniff the code, it'd be > useless after that.
The problem with this is it requires state on the server, which means extra database models, and on top of that those tables will need cron jobs to clear them out or something. This is especially a problem since hostile users can cause creation of rows in those tables - as many as they like, just by making a web request - though maybe I'm just being paranoid now. I wanted to keep the dependencies for this down to the minimum, and you can always replace it with something better.
I'm not familiar with James Bennet's registration app, so I can't comment on that front.
Luke
-- "I have had a perfectly lovely evening. However, this wasn't it." (Groucho Marx)
On Jun 28, 1:12 am, Luke Plant <L.Plant...@cantab.net> wrote:
> > alex.gay...@gmail.com said the following:
> > I'd suggest making the code to change the password a one-use-only
> > item though, so that even if someone did sniff the code, it'd be
> > useless after that.
> The problem with this is it requires state on the server, which means
> extra database models, and on top of that those tables will need cron
> jobs to clear them out or something.
Here's the way I usually solve this problem: send out a link that
looks like this:
There are three components to this link: the timestamp that the link
was sent out, the user's ID and an md5 hash of (timestamp + user_id +
SECRET_KEY). The timestamp is used to enforce a policy that says you
must follow the link within 24 hours of it being sent out. The md5
hash guards against tampering.
When the user clicks the link, they are taken to a one-time screen
that asks them to enter and confirm a brand new password.
With the above scheme you don't have to store state on the server and
you don't have to generate a random password for the user.
For added bonus points, instead of sending the timestamp as a full
base 10 integer use hexadecimal and the number of days since the epoch
- that allows you to represent the time in just a few characters which
can mean your entire reset URL fits within the 72 character line limit
imposed by bad e-mail clients such as Outlook (which can't handle URLs
that wrap).
You can try this scheme out for yourself by going through the lost
password recovery process on djangopeople.net:
> The problem with this is it requires state on the server, which means . . .
I don't think it's necessary to implement this in such a way that additional server state is stored. Instead, you could let the confirmation token be a hash of the internal user state -- including, most importantly, the user password's salt and encrypted values. That way, the valid confirmation token is 1) known only to the server (the User 'password' field is not externalized), 2) able to be computed at any time without being stashed anywhere, 3) constant until the user changes their password, and 4) guaranteed to change whenever the password is actually changed.
On Fri, Jun 27, 2008 at 8:12 PM, Luke Plant <L.Plant...@cantab.net> wrote:
> On Friday 27 June 2008 23:06:57 Collin Grady wrote: > > alex.gay...@gmail.com said the following: > > I'd suggest making the code to change the password a one-use-only > > item though, so that even if someone did sniff the code, it'd be > > useless after that.
> The problem with this is it requires state on the server, which means > extra database models, and on top of that those tables will need cron > jobs to clear them out or something. This is especially a problem > since hostile users can cause creation of rows in those tables - as > many as they like, just by making a web request - though maybe I'm > just being paranoid now. I wanted to keep the dependencies for this > down to the minimum, and you can always replace it with something > better.
> I'm not familiar with James Bennet's registration app, so I can't > comment on that front.
> Luke
> -- > "I have had a perfectly lovely evening. However, this wasn't it." > (Groucho Marx)
On Fri, Jun 27, 2008 at 8:31 PM, Simon Willison <si...@simonwillison.net> wrote: > I've got code for this lying round which I'd be happy to donate if > people agree this is the right approach.
I personally much prefer this approach. I've worked in a couple communities where personal attacks were quite frequent, and a common tactic was to claim a password was lost on someone else's account. It didn't give them access to the account in question, but it would adequately lock the person out if they happened to visit the site prior to checking their email.
Of course, sites like that also tended to have password change forms that accepted GET requests, and didn't have sufficient XSS protection. As you can imagine, wackiness ensued.
On Jun 28, 1:39 am, "Scott Moonen" <smoo...@andstuff.org> wrote:
> I don't think it's necessary to implement this in such a way that additional
> server state is stored. Instead, you could let the confirmation token be a
> hash of the internal user state -- including, most importantly, the user
> password's salt and encrypted values. That way, the valid confirmation
> token is 1) known only to the server (the User 'password' field is not
> externalized), 2) able to be computed at any time without being stashed
> anywhere, 3) constant until the user changes their password, and 4)
> guaranteed to change whenever the password is actually changed.
That's absolutely ingenious - that approach gets my vote. I think I'll
switch DjangoPeople over to that.
On Saturday 28 June 2008 01:39:26 Scott Moonen wrote:
> > The problem with this is it requires state on the server, which > > means . . .
> I don't think it's necessary to implement this in such a way that > additional server state is stored. Instead, you could let the > confirmation token be a hash of the internal user state -- > including, most importantly, the user password's salt and encrypted > values. That way, the valid confirmation token is 1) known only to > the server (the User 'password' field is not externalized), 2) able > to be computed at any time without being stashed anywhere, 3) > constant until the user changes their password, and 4) guaranteed > to change whenever the password is actually changed.
When I was writing the email, I almost included something about including a 'last modified' timestamp on the user object, so I was getting close, but not quite. This is great. The one slight issue is that if the user picks the same password (or ever does so in the future) then the hash could become the same again, and anyone who intercepted the email once would be able to reset the password again. However, this is fixable by including user.last_login in the hash of the internal user state. (We don't want to force people to use different passwords, and we can't do that properly anyway without keeping a history of password hashes).
The reset page then allows the user to enter a new password. It sets the new password (which *is* allowed to be the same as the last password) and updates the last_login timestamp.
I'm happy to implement -- I've got the tests setup already etc, so it should be easy enough.
Luke
-- "I have had a perfectly lovely evening. However, this wasn't it." (Groucho Marx)
> The one slight issue > is that if the user picks the same password (or ever does so in the > future) then the hash could become the same again,
I don't think that's true, at least using django.contrib.auth. The salt is re-generated whenever the password is changed, so the odds of the user picking the same password and obtaining the same salt are fairly close to zero. :)
Including last_login in the hash is certainly a fine idea, though.
On Sat, Jun 28, 2008 at 7:29 AM, Luke Plant <L.Plant...@cantab.net> wrote:
> On Saturday 28 June 2008 01:39:26 Scott Moonen wrote:
> > > The problem with this is it requires state on the server, which > > > means . . .
> > I don't think it's necessary to implement this in such a way that > > additional server state is stored. Instead, you could let the > > confirmation token be a hash of the internal user state -- > > including, most importantly, the user password's salt and encrypted > > values. That way, the valid confirmation token is 1) known only to > > the server (the User 'password' field is not externalized), 2) able > > to be computed at any time without being stashed anywhere, 3) > > constant until the user changes their password, and 4) guaranteed > > to change whenever the password is actually changed.
> When I was writing the email, I almost included something about > including a 'last modified' timestamp on the user object, so I was > getting close, but not quite. This is great. The one slight issue > is that if the user picks the same password (or ever does so in the > future) then the hash could become the same again, and anyone who > intercepted the email once would be able to reset the password again. > However, this is fixable by including user.last_login in the hash of > the internal user state. (We don't want to force people to use > different passwords, and we can't do that properly anyway without > keeping a history of password hashes).
> The reset page then allows the user to enter a new password. It sets > the new password (which *is* allowed to be the same as the last > password) and updates the last_login timestamp.
> I'm happy to implement -- I've got the tests setup already etc, so it > should be easy enough.
> Luke
> -- > "I have had a perfectly lovely evening. However, this wasn't it." > (Groucho Marx)
On Jun 28, 7:58 pm, "Adi J. Sieker" <a...@sieker.info> wrote:
> <si...@simonwillison.net> wrote:
> > That's absolutely ingenious - that approach gets my vote. I think I'll
> > switch DjangoPeople over to that.
> and to have the token expire you could add the date of the following day
> into the hash.
> That way the token is valid for max 48 hours.
Aah, I see what you mean - so when you check the hash you'd check it
using today's date, then if that fails check with tomorrow's date,
then if that fails announce the hash to be invalid. Neat, although it
isn't very flexible - if people want a policy that says the link will
work for 6 hours you'd need a different mechanism entirely.
I've been playing with this a bit and the one thing that was
concerning me is that the UID on large sites can end up being quite
long, potentially busting the 72 character length limit for the
overall URL. What do you think about using base36 encoding for the
UID? That can significantly shorten the number of URL characters
needed to represent large numbers:
This would also be a neat excuse to get base36 support in to
django.utils, further emphasizing Django's philosophy of really caring
about URLs. (base32, where 0, o, i, l and 1 are omitted as being too
easily confused, would be useful as well: http://crockford.com/wrmg/base32.html).
If you have to include an ID in a URL encoding it to take up less
space (and make its ID-ness a bit less obvious) is a handy trick - see
TinyURL, YouTube and Reddit for examples.
A further micro-optimisation is to leave out the hyphen entirely,
since an SHA-1 hash is always 40 characters long (we should probably
use that instead of MD5). You can cut off the last 40 characters and
use what's left as the base-36 UID.
If you add the timestamp into both the hash and the token then you can achieve a more granular expiration policy.
E.g., let's say the timestamp is something like '200806282255', indicating the reset token expires at 10:55pm local time on today's date. You can generate a token that looks like this:
The timestamp is thus directly available to the application for checking against the current time, but since it is also included in the hash, an attacker cannot transform a stale token into a fresh one.
-- Scott
On Sat, Jun 28, 2008 at 4:37 PM, Simon Willison <si...@simonwillison.net> wrote:
> On Jun 28, 7:58 pm, "Adi J. Sieker" <a...@sieker.info> wrote: > > <si...@simonwillison.net> wrote: > > > That's absolutely ingenious - that approach gets my vote. I think I'll > > > switch DjangoPeople over to that.
> > and to have the token expire you could add the date of the following day
> > into the hash. > > That way the token is valid for max 48 hours.
> Aah, I see what you mean - so when you check the hash you'd check it > using today's date, then if that fails check with tomorrow's date, > then if that fails announce the hash to be invalid. Neat, although it > isn't very flexible - if people want a policy that says the link will > work for 6 hours you'd need a different mechanism entirely.
On Jun 28, 10:01 pm, "Scott Moonen" <smoo...@andstuff.org> wrote:
> If you add the timestamp into both the hash and the token then you can
> achieve a more granular expiration policy.
That's the approach I use for djangopeople.net - the problem is that
including the timestamp lengthens the URL yet further. I actually use
a hex representation of the number of days since 2001/1/1 as a short
representation of a timestamp, which at least knocks it down to just 3
characters:
On Saturday 28 June 2008 21:48:24 Simon Willison wrote:
> A further micro-optimisation is to leave out the hyphen entirely, > since an SHA-1 hash is always 40 characters long (we should > probably use that instead of MD5).
MD5 is 8 chars shorter. Do we really need SHA-1? If I understand correctly, the only known vulnerability with MD5 is the ability to force collisions, but that will not help an attacker in this case. The only thing that an attacker can influence at all in the string being hashed is the timestamp, and it is limited to a few chars.
Your base36 stuff etc. all sounds good.
I implemented what has been discussed so far (apart from addition of timestamp) for my own project, with tests. It should be fairly easy to add to the newforms admin branch, but the only problem is knowing how to get some of the URLs without hard-coding them. (e.g. on the final page, you would want to have a link to the log in screen, but I don't know how to calculate that).
Regards,
Luke
-- "I have had a perfectly lovely evening. However, this wasn't it." (Groucho Marx)
On Jun 28, 11:21 pm, Luke Plant <L.Plant...@cantab.net> wrote:
> MD5 is 8 chars shorter. Do we really need SHA-1? If I understand
> correctly, the only known vulnerability with MD5 is the ability to
> force collisions, but that will not help an attacker in this case.
> The only thing that an attacker can influence at all in the string
> being hashed is the timestamp, and it is limited to a few chars.
Good point, well made - MD5 should be fine (I suggest leaving that
justification in a comment somewhere to fend off the inevitable
complaints).
Thanks Simon, for the idea of using a timestamp in the url and in the
hash. A really good idea.
You could shorten the hash to 6 digits by using the HOTP algorithm
(http://www.openauthentication.org/). If we send it in base32 it will
be even shorter. I've got the Python code for HOTP ready and would be
happy to contribute it. The nice thing is that HOTP has been studied
and at the moment can be considered safe.
On Sat, Jun 28, 2008 at 4:48 PM, Simon Willison <si...@simonwillison.net> wrote:
> On Jun 28, 11:21 pm, Luke Plant <L.Plant...@cantab.net> wrote: >> MD5 is 8 chars shorter. Do we really need SHA-1? If I understand >> correctly, the only known vulnerability with MD5 is the ability to >> force collisions, but that will not help an attacker in this case. >> The only thing that an attacker can influence at all in the string >> being hashed is the timestamp, and it is limited to a few chars.
> Good point, well made - MD5 should be fine (I suggest leaving that > justification in a comment somewhere to fend off the inevitable > complaints).
You don't have to choose, because of the way secure hashes work you can take any substring of the hash (the bits flipped due to different input have to be randomly distributed over all bits). So if eliminating 4 bytes is worthwhile, you could just cut them off SHA-1. This is a common technique in security applications when generating keys which are shorter than 20 bytes.
On Jun 29, 9:42 am, Simon Willison <si...@simonwillison.net> wrote:
> On Jun 28, 10:01 pm, "Scott Moonen" <smoo...@andstuff.org> wrote:
> > If you add the timestamp into both the hash and the token then you can
> > achieve a more granular expiration policy.
> That's the approach I use for djangopeople.net - the problem is that
> including the timestamp lengthens the URL yet further. I actually use
> a hex representation of the number of days since 2001/1/1 as a short
> representation of a timestamp, which at least knocks it down to just 3
> characters:
Just out of interest, the method I use is to salt the password with
the current UTC date (not time). Then the server just checks the token
X times, reducing the checked date by one day each loop.