Thanks for your response. The details about your process are really informative too.
Apologies for this lengthy post, but a lot of the OWASP recommendations seem wise and yet introduce substantial complexity and effort. I'm thinking if these recommendations can be implemented/encoded once, it would be a nice library for use again and again.
Bonesnap;11051831 wrote:This is how I implemented password resets in my current project:
3) A password reset token is generated, along with the pertinent information (email address, user ID, timestamp, etc.) and stored in the database. The token is only valid for 1 hour.
I'm guessing these password reset tokens are stored in their own table? I was thinking something like this:
id
user id
token, probably a lengthy, randomly-generated alphanumeric key. something hard to guess or forge.
nonce/secret chars - some random sequence of chars which can be used to improve security of password reset email (see below)
* expiration datetime
Bonesnap;11051831 wrote:4) An email is sent to the email address that was entered with a link to the reset page. Link contains the token. Link also contains an additional variable "r" which equals 1. More of a distraction than anything. This must be present and must equal 1 otherwise the reset page won't load.
I tend to doubt the "r=1" is going to help. I might suggest a nonce or something based on timestamps or a hash of some user's email or something but I'm not sure it's going to add anything to the token. If the token is truly random, that's about as safe as you're going to get I would think. If someone intercepts the email to get the token, they also get the link too. On the other hand, if someone exploits the database but not the php code, then they may not know how the nonce gets calculated which would provide a tiny bit more defense. (is this what we mean by 'defense in depth?')
Bonesnap;11051831 wrote:5) User receives the link, clicks it, and is taken to the reset password page where he can enter and confirm his new password.
If I correctly understand the OWASP recommendations, as poorly written as they are, they propose using the security questions at this point rather than just letting some email recipient (or thief) reset the password.
Bonesnap;11051831 wrote:6) Upon resetting his password, the token is invalidated (along with any other tokens that might be associated with that email address) and a confirmation email is sent.
Thank you for assiduously including this detail of your process. It might be easily overlooked. It's important to clean up the loose ends for security and other reasons.
Bonesnap;11051831 wrote:I was thinking about limiting the password reset to the IP that requested the reset, but wasn't sure how that would affect mobile users who are not connected to a wifi network.
Or users who turn off wifi and walk outside to get in their car. Yeah it's been my experience that insisting on a consistent IP can lead to what seem like mysterious failures: the system suddenly doesn't work because my IP changed (and I didn't even know it).
Bonesnap;11051831 wrote:From what I read my system isn't 100% secure and I should be hashing the token that's being sent to the user but I fail to see the added benefit.
Let's admit it: no system is 100% secure π
As for 'hashing' the token, refer to my nonce suggestion above. First time I saw the use of a nonce was in the Authorize.net payment gateway. The API required with each POST that requested payment a digest/hash/nonce which was calculated from some values, I forget what they are. For better security, I would imagine it should be something that hashes together the password-reset-token and something that does not exist in the email which a hacker could not easily guess. Basically, when some dude shows up on the password reset page trying to reset a password, you hash the token with something in your db that wasn't in the email (a user id? perhaps some random salt or chars you stored in your db?). Note that the token's hash would need to calculate identically between the time it is requested from the user and the time they return to your site. You might consider storing the server-side secret chars in your password-reset-token-table (see my suggestion above).
Bonesnap;11051831 wrote:Indeed, the token itself is obscured but so what? I mean, all this is predicated on the fact that the email account hasn't been compromised
From what I see the token is not obscured -- at least it's not if you have the email. It would be right there in plain sight.
Bonesnap;11051831 wrote:but my Spidey-sense tells me that if someone's email account is compromised the owner is probably not going to care quite as much about some random service and instead would rather get their email account back. Also, from our perspective, there's not much we can do about compromised email accounts.
In my case, the random service might allow the purchase of quite expensive--although not exactly fungible--merchandise. For most of my projects, the password reset without security questions would be entirely adequate.
Bonesnap;11051831 wrote:I suppose that's what these secret questions are for, but from everything I have read they are way worse than passwords and offer very little protection. Usually the selection of the questions is limited (and the questions are too similar to other websites). Sometimes there's the ability to enter your own. But then there's the challenge of validating these answers. What if the question was "Who was your favourite high school teacher?" and you put "mr. smith", and then 8 months or longer later, you enter "Mr. Smith". Should that validate? Should the answer be exact? Probably should be, right? But then you run into these issues. Seems like a very large can of worms that doesn't need to be opened.
I certainly concur it's a can of worms but would point out a few things:
Security questions are an additional layer of protection for password reset sequence. They are not used instead of but rather in addition to the email reset loop as an added precaution.
The OWASP article has some decent thoughts on what good questions are, and it discourages letting users define their own secret questions.
* I was assuming I would check answers case-insensitively. Remember that this is just a tiny added layer of security between the password reset email and actually resetting the password.
Some other things about the OWASP recs:
OWASP wrote:be sure that you collect some means to send the password reset information to some out-of-band side-channel, such as a (different) email address, an SMS texting number, etc.
This to me is where things start to get kinda messy and we start to introduce a lot more logic branching. E.g.:
$other_email = DB_other_email::fetch($blah);
if ($other_email) {
// use other email;
} else {
send_token_via_text_message_to_phone_number_on_file($token);
}
// etc
// etc
// etc
Does anyone do this? I can appreciate that it might increase security, but it might also reduce it. Another email address or communication channel exposes another attack angle.
OWASP wrote:Review Any Canned [Security] Questions with Your Legal Department or Privacy Officer
WTF? Legal liability can be introduced by offering canned questions? sigh.
OWASP wrote:we would always recommend at least encrypting the answers rather than storing them as plaintext
Not something that had ocurred to me. I'm thinking convert the answer to lowercase before using [man]password_hash[/man]. OWASP also recommends storing the security questions as reversible (i.e., decryptable) ciphertext.
OWASP wrote:Periodically Have Your Users Review their Questions
While this makes sense, questions arise. What does 'periodically' mean? At what point in one's web application would one prompt a user to do this? After login? On every page access? It also implies that we store a timestamp/datetime with every answer. NOTE: if you use password_hash when storing answers, you can never present them with their previously entered answers.