2

What are the pros and cons of the different ways of handling reset links? I see two ways of handling them:

  1. Generate a random string, for example with uuid4. Store it in the database with the user and send it in a link to the user. When the form is filled in and submitted the random string will be the payload and matched to the user. The string is then removed from the database so it can only be used once. It should also have an expiry date.
    • Pros:
      • Simple and safe
    • Cons:
      • Have to store it in the database
      • A malicious user can cause database writes by repeatedly using the "forgot password" feature. This would have to be mitigated by a maximum possible tokens per day feature and that still only limits it for one account. If the token is overwritten then storage space will not matter but it could still cost money in form of database writes.
  2. Generating a signed token. From the username or id and a timestamp a signed token is generated with a secret key. This is the link sent to the user. When the user submits the token is decrypted back and the user and timestamp retrieved and validated. The issue here is that it can be reused so you have to store the old password in the link as well (it would be a salted hash). I don't like the idea of being able to reuse it, what if it is used on a public computer? Then the user has to clear the browser history, bad!
    • Pros
      • Nothing needs to be stored in the database.
    • Cons
      • Riskier? Is it a bad idea to send the old, encrypted (salted hash) password in the link? If "last time modified" is already stored then that could be sent instead without writing to the database upon the reset request.
1
  • There is a third way. Use method two, (without the password hash) and once the signed token is submitted for the reset, then store* it in the database until the expiry time is reached. Then you only have database reads, and the writes only happen when a user who has access to the email account submits it for the reset, minimizing the chance for abuse.
    – Xander
    Feb 24, 2017 at 21:26

6 Answers 6

2

I'd go option 1 all the way - you want to ensure that a given token is only used once and it is not possible to do that without a database entry.

2

Each is valid - as long as your random value is sufficiently random and the encryption/signing uses appropriately strong algorithms. You've already started thinking about specific problem scenarios - I would encourage you to continue.

If we assume there is a risk of the values being predictable (or possible to brute-force) or some other (limited) vulnerability on the site then the random value has some advantage. Since it is stored serverside, it also serves to flag that the account is in a changeable state. Exploits targeting the renewal process will only be effective against what should be a small proportion of the accounts. However this is very much an edge case.

A problem I have encountered with the random value is this: I can't remember my password, I click on the reset button, I wait, no email,so I click on the link again. When the reset email it arrives it doesn't work! (because its from the first reset, and a second email is on its way with the current code). OTOH to block a second reset attempt strikes of over-confidence. Perhaps a pragmatic solution is a refractory period after the last reset where a further reset cannot be carried out.

A further consideration is that I could probably put up with transcribing a UUID from my messaging device to my browsing device (if for some reason I couldn't or didn't want to cut and paste between them and couldn't click through from the messaging device) but an encrypted, signed message is going to be much, MUCH longer.

Regarding the public computer issue - I don't think many people would use a dedicated MUA on such a device - so the risk of a finger-print only arises if a web-based email client uses content caching heavily (erk!) or the authenticating system is not using TLS (erk!) or (and this potentially applies to non-public devices) the terminal is compromised. I would expect in most cases that having accessed the message containing the token, the user will then endorse it - and with the old password embedded in that token, it ceases to be re-usable. It's OK to embed the salted, hashed password in the token using appropriately strong hashes and salts, but it does give me an uneasy feeling. We used to think 3DES and MD5 was secure. But there are alternatives to sending the hashed password to prevent replays. The data is just representing the state of the account record at a specific point in time. You could use a version number or date. But that does mean writing data serverside.

1

Option 1. This is really the way to go and a well known practice. You definitely want a time-limited nonce. Also have a read here: https://www.troyhunt.com/everything-you-ever-wanted-to-know/

0
1

One flaw with the second approach (signed/HMACed value) is that it creates a single point of failure in your auth system: if an attacker ever manages to steal that signing/HMAC key, you're screwed. The attacker can then generate valid password reset tokens for arbitrary accounts.

It's usually easier to read data out of a server (arbitrary file include bugs, such as XML external entity vulns, are pretty common) or build system (if your source code isn't secure enough, or somebody puts build artifacts on a publicly-readable FTP, or somebody compromises your backups, or something) than it is to write to a database (and if the attacker can write your database, you're already toast; they can just reset passwords directly). This is part of why key rotation is a good idea, but if somebody steals your key before it gets rotated out they could do a hell of a lot of harm with it, fast.

0

Generating a signed token. From the username or id and a timestamp a signed token is generated with a secret key. This is the link sent to the user. When the user submits the token is decrypted back and the user and timestamp retrieved and validated.

You are talking about secret keys, "signed" tokens and "decryption" (and later, "salt") in a very loose manner that suggests you are likely to implement this wrong. What you would want to use in this case is a message authentication code, for example HMAC-SHA256, which your cryptographic libraries almost certainly support already.

You are very wise to be on the lookout for replay attacks. Your idea to send the current password's salted hash as context, while not good if taken literally, is on the right track. What you're missing is that you don't need to put the hash in the URL because you're already storing it; all you need to put in the URL is information that allows your server to retrieve the salted password hash when the reset URL is clicked. The username that you're resetting the password for presumably would do the trick.


Here's a sketch of one possible solution. First, you need a secure secret_key. One way of doing this that can save you a lot of headaches is to use ephemeral keys—instead of having a long-term key that's used for a long time and that an attacker may still, what you do instead is when your password reset service starts up, it:

  1. Selects a 128-bit secret_key at random. This will only be ever kept in memory—never write it to disk or over the network.
  2. Select an unique startup_uuid. I'd use a random number or UUID.

This means that the secret_key is only valid for the lifetime of a key reset service process—every time you restart it the key gets discarded and a new one generated. The upside of that is that your service is much more resilient against keys getting stolen. The downside is that reset links generated before a restart will not validate—the purpose of the startup_uuid is to detect that case and handle it in a friendly fashion.

When asked for a password reset URL for username, the password reset service does this:

  1. Choose an expiry timestamp for the reset link, e.g., 15 minutes later the current time.
  2. Look up the request's username's password entry and retrieve its current salted password hash.
  3. Construct an unambiguous delimited message reset_data with the following fields in some fixed order:
    • The server process' startup_uuid;
    • The request username;
    • The selected expiry timestamp;
    • The user's current password hash.
  4. Compute reset_token = HMAC-SHA256(secret_key, reset_data)
  5. Construct a reset URL with these values as parameters. (NOTE: the password hash is not a parameter in the reset URL!)
    • The username;
    • The server startup timestamp;
    • The expiry timestamp;
    • The reset_token.
  6. Return that constructed URL.

When you receive a request at the password reset URL's endpoint:

  1. If the request's startup_uuid is not equal to the server's, then the service has been restarted since the link was generated. We no longer have the old secret_key and thus cannot verify that request. The user will have to redo the password reset process.
  2. If the current time is later than the URL's expiry timestamp value, the link is expired, so don't accept it.
  3. Look up the salted password hash for the request's username.
  4. Construct an unambiguous delimited message reset_data as above.
  5. Verify that this reset-data message matches the reset_token that's included in the URL. Make sure to do this safely with a constant-time equality comparison.
  6. If the verification succeeds, go ahead and reset the user's password.

This protocol has these virtues:

  • The secret keys are ephemeral and only ever kept in memory, so even if somebody somehow steals a key it's only good until the next restart.
  • The hash of the password to be reset is used as an input to HMAC. If an user tries to reset a password that's already been reset or otherwise changed, the reset token verification will fail.
  • Unlike your proposal, the hashed password is never disclosed.
0

I would lean toward option #1 for the following reasons:

  • It is forward compatible with more security features. For example, the reset code could be delivered out of band (e.g. SMS). The reset code could be tagged with IP address or geolocation information in your database, and you could restrict reset requests to the same location. You can count reset attempts and evaluate those when committing high risk transactions. Etc.
  • It is easier to implement. If you want to pass a token instead of a code, the token would not only need to be signed but also encoded in a way that it will pass correctly through email firewalls, DLP systems, proxies, etc. One incorrect character sequence and your token could be blocked or mangled by other security features. All the time working through those issues could be directed instead at focusing on more security enhancements.

If you are worried about too many database inserts, just restrict a user to a handful of codes on a FIFO basis. For example, GMAIL restricts you to 10 backup codes.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .