
Password Hijacking Security Incident and Response - mazsa
http://blog.heroku.com/archives/2013/1/9/password_hijacking_security_incident_and_response/
======
tptacek
Nobody ever gets password resets right. They're #1 in our list of 7 Deadly Web
App Features. Even if you think you understand your password reset, and can
fit all its workings in your head, I recommend you go look at it closely
again.

~~~
codegeek
Any good reference to look at in terms of best practices for password reset ?

~~~
tptacek
No; every implementation fails in some new and creative way.

The implementation I feel most comfortable with:

1\. Generate a 256+ bit cryptographically secure random number and base64 it
to create a token.

2\. Record that token in your database, timestamped, along with the user
account for which the token was requested.

3\. Mail the token to the user's email address.

4\. When the user returns to the site after recovering the token, use that
token to look up their account from the database.

5\. Expire tokens within single-digit hours so users don't end up accidentally
banking password-equivalents in their email accounts.

6\. When a user changes their password or requests another password reset,
expire all tokens already associated with their account.

I would also recommend:

(a) Not having any in-band administration functionality in your application;
instead, have a separate admin application, attached to the same databases,
available only on a VPN.

(b) Require 2-factor authentication (such as Duo Security) for both admin VPN
access and admin login.

A knock-on benefit of (a): your admin functionality is easier to build,
because it doesn't have to do the UI/UX chinups your normal exposed app code
has to do; crappy looking admin screens nobody but your employees see are
generally fine.

~~~
wulczer
I commented on the other thread, but will repeat here: I really like how
Django handles password resets.

No nonce is generated and nothing is stored. The user is emailed a link with
her user ID and a token that's a hash of (last login timestamp + the user's ID
+ the user's (hashed) password + current timestamp). The token is HMAC-signed
with the site's secret key.

This way the token automatically expires if the user either successfully
changes her password (the password hash will change) or manages to log in
(last login timestamp changes).

It seems that in Django password reset tokens are valid forever, but it would
be trivial to add the current timestamp to the token and include it when
computing the HMAC signature; then the password reset form would check if the
token has been generated recently enough.

I like this method because you never need to touch the database and store
tokens; it's all fairly stateless.

~~~
tptacek
If you want to get clever to avoid touching the database --- which is why
every broken password reset feature ever conceived decided to get clever ---
you should have your code assessed professionally. I'm not telling you it's
impossible to build a secure password reset that doesn't simply store a token
in the database. I'm just saying the cost/benefit payoff is probably not
there.

Personally, I have this particular bit of appsec down cold, and if I was
building a new app, I wouldn't even think about it: I'd use a random token and
save it in the database.

~~~
wulczer
I just gave django.contrib.auth.tokens a read and shared my opinion on it :)

I wouldn't roll my own password reset feature if I can just take the builtin
one from Django, which is what I did.

~~~
tptacek
The good thing about taking your web stack's password reset feature, if it has
one, is that you're probably going to find out quickly if there's a bug
discovered in it.

Note though that that's _not_ the case for 3rd-party password reset libraries
or, more likely, the all-purpose security library that provides it. I'd be
very wary about using a 3rd party library for password reset unless they've
got a credible for story for it having been reviewed.

Django: Good.

3rd Party Library: Less Good

Just Using A Random Token: Good

Cryptography: You Will Perish In Flames

~~~
thibaut_barrere
Does Devise [1] qualifies as a properly reviewed 3rd Party Library, to your
knowledge?

Asking this since it's probably the most widely used authentication gem in
Rails etc...

[1] <https://github.com/plataformatec/devise>

------
vld
It is very pleasing to see companies like heroku publicly thanking (we even
got a blog link here) the security researcher that initially reported a
vulnerability. Hopefully, we'll see more of this in the future.

------
laurencei
What am I missing here? This is what I do; \- User resets password, email link
to user \- User clicks link, random temporary password emailed to user \- User
logs in with temp password, system then asks them for new password.

This way there is no risk of someone somehow guessing the reset link. Even if
they do, all it does is email the user, so they gain nothing...

~~~
tptacek
Random temporary password is a terrible idea. It is both less usable _and_
less secure than sending a secure random token and asking the user to provide
a new password. Many users will never change the temporary password. All of
them will leave the password in their mail spool. Without even more database
signaling, the "resets" will never expire, because they are just passwords.

Don't do it this way.

~~~
laurencei
No - the system first sends the secure random token, then instead of asking
them for a new password then, it instead auto-generates a random password and
emails it to them.

Then the system FORCES them to change the password when they login with the
new password.

Oh - and the tempoary password only valid for 24 hours.

So you get the best of both worlds - without anyone being able to 'guess'
anything able to do anything...

~~~
tptacek
The system forces them to change the password if they actually log in. If they
don't, the password just sits there. And some users will just cut/paste it.

I'm sure there's a million fiddly things you can do to address the weaknesses
of temporary password issuance, but you'd be better off sending a semantically
meaningless random token. All the countermeasures you're thinking of here
apply identically to the token.

I am advocating for the reset scheme that is the hardest to mess up. Yours is
not the hardest to mess up. I'm not trying to get you to change yours.

------
codegeek
How is this for a "decently secure" password reset method ?

\- User enters email address to reset password

\- A reset link (uniquely hashed,salted etc.) is generated server side tied to
that specific email address. This link can only be used once and also has a
expiry date if un-used by that time.

\- If this reset link is accessed (hopefully by the intended user), A form is
presented to the user where it asks to enter the email address, new password.
If entered email address does not match the original email, user gets an
error. Immediately, all sessions are invalidated/reset if any. If entered
email address matches, then reset the user's password. Again, invalidate/reset
any existing sessions.

\- After resetting password, never login the user directly. Ask them to login
manually again.

~~~
tptacek
The only way this fiddly scheme could be secure is if the randomizing
information (that prevents an attacker from brute forcing the hash against all
possible email addresses) is different for every token and thus stored
somewhere serverside. It is no more secure than simply sending the user a
random token, but has all the logistical challenges of storing a token.

So just use the token.

~~~
codegeek
"stored somewhere serverside"

So you are a strong advocate of generating the token and storing it in the db.

~~~
tptacek
That is the simplest possible answer and so it's the one you should use.

------
codegeek
"While both Mr. Sclafani and Heroku endeavoured to use test accounts
exclusively, a very small number of customer account passwords were reset
during the incident"

This is interesting.

~~~
jmillikin
Likely due to "related issue in the password reset flow that could be used to
reset the passwords of a certain subset of users at random"

I would have liked to see more details about what was going on to cause that
sort of problem. I can't think of any reasonable code that would cause
password resets for an empty ID to be assigned to a random account.

~~~
patio11

      #Start a password reset
    
      @user = User.find_by_email
      @user.update_attribute :password_reset_nonce, rand(16 ** 16).to_hex
      #mail them the password reset email
    
    
      #Look up a user for a password reset
    
      @user.find_by_password_reset_nonce params[:password_reset_nonce]
      #if nil, above line returns random user on some databases.  Oops.
      session[:user_id] = @user  #Logs in user
      #We assume if they know the nonce that proves they own email.
      redirect_to passwordReset_url
    

I've elided additional code which might theoretically be used to make the
nonce expire and to prevent re-use for brevity, but it's possible that neither
of these measures would fix the issue.

Bugs like this one always make me think "There but for the grace of God go I."

~~~
jmillikin
It's the "if nil, above line returns random user on some databases" I'm
curious about. What databases behave thus? It's a very problematic behavior.

~~~
Xylakant
It may be evaluated as "SELECT * FROM USERS WHERE password_reset_nonce IS
NULL". If your password_reset_nonce field may contain null values, this will
fetch the first row of those it finds. Some databases show quasi-random
behavior when fetching that row.

