Take the 2-minute tour ×
Information Security Stack Exchange is a question and answer site for Information security professionals. It's 100% free, no registration required.

I am building my login form action, and I want to make sure my logic is solid and as secure as possible.

1.) First I check if the provided e-mail address exists in database, if not, I show a generic error message Invalid e-mail address or password. and exit. Notice, the error message does not specify that the e-mail does not exist.

2.) Then I check if failed_logins which is stored in the database is greater than 5. If it is, I show an error message User has been locked out due to excessive invalid logins. and exit. Notice that this message is shown if a user account is locked out, but the incorrect password was provided. I did this because if an account is locked out, I don't want a potential hacker to be able to determine if they provided the correct password.

3.) Check the password comparing the bcrypt hash in the database with the password provided. If incorrect, show Invalid e-mail address or password. increment failed_logins in the database and exit.

4.) Check the flag activated in the database. If activated is set to 0, then show an error message User is not activated. Notice, they have to provide the correct password to see this error message.

5.) Valid, set failed_logins back to 0 in the database and set the sessions.

Is there anything in this logic that is flawed or insecure? Thanks much.

share|improve this question
I am not sure what you hope to get form this. There is still plenty of room for failure. –  Rook Nov 8 '12 at 4:12
what happens if you attempt 5 times on an email that doesn't exist in the database? –  rofls Nov 8 '12 at 6:29
An e-mail that doesn't exist will always return Invalid e-mail address or password even after 5 failed attempts. I suppose that does provide a hacker a way of checking if an account e-mail exists. Is there any risk there? –  Justin Nov 8 '12 at 7:37
If you are resetting failed_logins when the correct email + password are provided. Then even if they lock out the account for trying 5 wrong passwords, what if they get it right on the 10th try? You are letting them in, or if you aren't letting them in then how does failed_logins ever get reset? –  asbumste Nov 8 '12 at 18:34
Just out of curiosity, is there a standard way for this, instead of a developer coming up with their own mechanisms ? –  sudhacker Nov 8 '12 at 23:58
show 2 more comments

4 Answers

Some attacks to think about:

Tricking the failed_logins by clearing your session id. Enumerating accounts by trying to lock out existing accounts. DoSing administrative accounts by locking them out with failed login attempts. Enumerating login names with the forgotten password feature or with registration, or insecure direct object reference. Spoofing your ip address by supply a x-forwarded-for http header element.

Broken session management: Session Riding (CSRF), Session Expiration, Session Fixation, Problems with Cookie Domain/Folder Scope, Storing Session ID's in plain text in the database, Insufficient entropy in session ids. Not setting the "secure" and "HTTPOnly" flags. Clickjacking. OWASP A9...

Did I miss anything?


share|improve this answer
"Enumerating login names with the forgotten password feature or with registration" Most sites have this "weakness," and it's not really something you can get around without hurting practicality. eg: I have many accounts on sites I don't care too much about and I normally use one of a few different "spam" emails for those accounts. Sometimes I forget those passwords and it's really useful to know which email I used on registration so I can get reset my password. –  Null Nov 13 '12 at 17:45
@Null your right most sites have this vulnerability, its a CWE-204 violation. Most sites are also vulnerable to XSS. Its easy to patch, Google displays a captcha after too many failures. You should really familiarize your self with the CWE system, as it is the basis for the entire Application Security industry. I think you have complained to me before about CWE violations... –  Rook Nov 13 '12 at 18:08
I just don't think this "weakness" has much merit when the exposure is intentional and serves a practical purpose. And no, I don't think I have complained about CWE violations. –  Null Nov 13 '12 at 18:14
@Null It provides value to an attacker, its easy to test for, easy to prevent, and easy to put in a pentest report. –  Rook Nov 13 '12 at 18:15
How do you prevent it without making it a PITA for legitimate users? Using a CAPTCHA would still "provide value to an attacker," it would just slow them down a little. –  Null Nov 13 '12 at 18:17
add comment

You should swap steps 1 and 2 round. You should keep a separate record of login attempts per provided email address whether that email is valid or not. Use the same case sensitivity rules as your user database for email addresses.


invalid@example.com, anypassword, = 1 failed_login
invalid@example.com, anypassword, = 2 failed_logins
valid@example.com, invalidpassword = 1 failed_login
valid@example.com, invalidpw2 = 2 failed_logins

You could reset the failed_login count per email address after a random amount of time (then an attacker cannot infer that an email address is not valid as the locked out message persists - this "simulates" a valid login for that email address in the eyes of the attacker).

Also, make sure the code to generate the whole presentation of the error message is set in the same piece of code. If Invalid e-mail address or password is shown, but is generated in different parts of the code there may be subtle differences in the HTML that the attacker could detect and use to their advantage. e.g. if it failed at step 1 it might be generated as <span>Invalid e-mail address or password</span> or if it failed at step 3 it might be <span id="message">Invalid e-mail address or password </span> - you do not want these detectable differences as the attacker can then infer which stage their login attempt got to, so it is best to use the same code.

Also, you could introduce a timing so your login response always takes the same amount of time to return to the user. You could set a timer at the start of the process, and check the timer at the end (whichever step it gets to). Then if the whole process is less than a set time (e.g. 1.5s), the thread is delayed until the total amount of time has passed. This will stop "blind" detection of which login step was reached due to server response time.

share|improve this answer
Yeah, right now it takes significantly longer if they get to step 3, bcrypt takes over 1 second. I like your idea of moving failed_logins out of the users table, and simply create a new table which tracks all failed logins, even if the user account does not exist. –  Justin Nov 9 '12 at 2:14
add comment

Confidentiality. It sounds like you are trying to prevent an attacker from testing a particular email address to see whether it exists in your database. However, your scheme has a hole: it doesn't actually achieve this.

Given an address, say someone@somewhere.com, the attacker can try logging in with that email address 6 times (using some unlikely password). Then, the attacker can try logging in once more.

  • If the attacker receives a Invalid e-mail address or password. response, the attacker can infer that this email does not exist in your database.

  • If the attacker receives a User has been locked out due to excessive invalid logins., the attacker can infer that this email does exist in your database.

Integrity. Your method for validating passwords seems fine, and should prevent an attacker from doing a dictionary search on a particular user's password.

It will not prevent an attacker from making a small number of tries at a large number of users' passwords. Suppose that an attacker can somehow enumerate a set of email addresses who have a login with your service (maybe by some clever websearch, or by exploiting the confidentiality breach above, or something; I don't know whether this is actually possible in your case, but let's explore the consequences if it is). Then an attacker can iterate over all of those accounts, and make 5 guesses at the password for each account.

Recent research has found that an attacker who can make 10 guesses for each account will be able to compromise about 1% of accounts. Extrapolating, we can expect that in your situation, an attacker will be able to compromise about 0.5% of accounts, by making 5 guesses at the password for each account that the attacker is aware of.

You can make this kind of guessing attack harder by keeping track of the number of failed login attempts per IP address as well, and temporarily blocking logins from IP addresses that have a large number of failed login attempts. However, this is not a perfect defense (an attacker with a botnet can easily circumvent this).

Availability. It would be easy for an attacker to lock out a targeted user: all the attacker has to do is try logging in 6 times with an unlikely password, and now that user won't be able to login without contacting your support team. This might be an acceptable risk. The good news is that if this happens frequently, you will know it, and you can put in place defenses at that point.

Standard solutions. You asked if there are any standard solutions. As far as I know, there are no standard solutions. However, in a related vein, you might be interested in this academic paper, which tries to avoid use of popular users: if too many other users have chosen the same password, new users are not allowed to use it. The paper has some clever data structures to make sure this doesn't create new security problems.

share|improve this answer
add comment

Since you want to hide whether a given email address exists or not in the system, you will need to make scenarios 1 and 3 more similar. @SilverlightFox suggests adding a pause so that the time taken by the login process is always the same -- that's a step in the good direction, but not sufficient. Indeed, when a thread sleeps, it does not use the CPU, which is free to do other tasks. An industrious attacker will manage dozens or hundreds of login attempts in parallel, and will make your server crawl at snail pace. Under these conditions (very heavy load), any attempt at hashing a password (with bcrypt) will take a very long time -- i.e. a very detectable time.

Therefore, in order to hide whether a given email address is a valid login name or not, you must arrange for always computing bcrypt. Also, you do not want to yield the same information through the "account blocked" message. Things should go like this:

  1. You always record number of attempts per email address, whether the email address exists or not in your system. I know that this is inconvenient (in database terms, you will need another extra table). If the attempt count for a given email address exceeds 5, you reject the login.

  2. You have a look at the database, to see if the email address exists in the system, and obtain the bcrypt salt. If it does not, you use a fixed extra salt. Then you unconditionally compute the bcrypt part. Then (and only then), if the bcrypt output does not match the stored value, or if the fixed extra salt was used, then you reject with "Invalid email or password".

  3. If the login attempt succeeds, you reset the "attempt count" for that email address.

Now, the extra table for keeping track of attempt counts can be inconvenient in many ways, because it may grow quite fast. You could make a variant in which you do not have the table. Instead, you can keep the count for each valid user, but then you must reject login attempts with "Invalid email or password" when the count is at 5, whether the bcrypt output matched or not. In other words, you can afford to yield to the user some information about whether his account is locked or not only if that information is computed regardless of whether the account exists or not (through the extra table).

It can also be said that locking accounts for a server with remote logins is a bit harsh, because it would allow anybody to lock anybody's account, by mistake or malice. Locking entails manual unlocking, i.e. extra helpdesk costs. I suggest automatically resetting all "attempt counts" every two hours: thus, locking is only for two hours, which is enough to deter dictionary attacks while making the whole system more robust to denials of service.

share|improve this answer
add comment

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.