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.
(Related tickets:
http://code.djangoproject.com/ticket/4235
http://code.djangoproject.com/ticket/5272 )
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."
Luke Plant || http://lukeplant.me.uk/
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"
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)
Luke Plant || http://lukeplant.me.uk/
The problem with this is it requires state on the server, which means . . .
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.
-Gul
> > 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).
So, the URL would end up like:
https://example.com/reset/34-7127f83ebf8ce7ed22bdc50a50572a30
i.e.
https://example.com/reset/{uid}-{hash}
where
hash = sha1(settings.SECRET_KEY + str(user.id) + user.password +
str(user.last_login))
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.
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,
> 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.
adi
--
Adi J. Sieker mobile: +49 - 178 - 88 5 88 13
Freelance developer skype: adijsieker
SAP-Consultant web: http://www.sieker.info/profile
openbc: https://www.openbc.com/hp/AdiJoerg_Sieker/
reset_token = timestamp + hash(timestamp + user.password + ...)
> 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,
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.
Craig