Common Information Disclosure Vulnerabilities During Registration and Authentication

Let’s face it. We all have secrets. That’s not necessarily a bad thing. There are just certain pieces of information you don’t want everyone to know – your phone number, checking account number, social security number, and ATM code.

Most people recognize that you shouldn’t share certain information with people that they don’t have a trusting relationship with. However, there are others that seem to be open books and willingly share everything that they shouldn’t. In other words, there is no TMI filter.

When a person or a system shares secret information that they shouldn’t, they are disclosing sensitive information. This is known by a number of different names: information leakage, sensitive data exposure, and information disclosure, just to name a few. Regardless of what label you put on it, when a system shares something that should be kept secret, this is a very bad thing.

As a security professional, I can tell you that one of the most dangerous areas that a system can divulge secret information is during the registration and authentication process. In this post, I will share some of the most common sources of information disclosure in these processes.

For information why user enumeration is so dangerous, read my article entitled, “The Dangers of User Enumeration.”

1. Email Enumeration During Registration

How do you handle it when a user tries to register with an email address that is already used in your system? If you even remotely suggest to the user that the email address is already in use, then you have a critical vulnerability in your system.

Why is it so dangerous to let someone know that an email address is already in use? An attacker can use this information to mine existing users from your system. Once an attacker has a reasonably sized database of existing users, it is trivial to launch a Password Spraying attack. The probability of an attack like this succeeding is fairly good, since the attacker will attempt to login with numerous accounts at a time with the most commonly used passwords.

The Wrong Way

Developers usually let a user know that an email address is in use as a last resort. The reason is simple: they don’t know a better way to handle the situation. The flow usually goes like this:

  1. A new user navigates to the web application.
  2. The user doesn’t have an account so s/he proceeds to the registration screen.
  3. The user enters his/her information, including email address, and attempts to register.
  4. The web application backend finds that a user with the supplied email address already exists. What should you do? Many erroneously choose to return an error.
  5. The user receives an error message indicating that the email address is already in use.
  6. Attacker leaps for joy and throws a party in your honor.

The Right Way

So how do you handle this situation correctly? The best option is to accept the email address during the registration process without error. The system can then send an email message to the user indicating that they have already registered with that email address. The email can even include a link that the user can click to reset the password.

The new flow looks like this:

  1. A new user navigates to the web application.
  2. The user doesn’t have an account so s/he proceeds to the registration screen.
  3. The user enters his/her information, including email address, and attempts to register.
  4. The web application backend looks for a user with that supplied email address. One of two things happen:
    1. If the email address is already in use, an email is sent to that address indicating as such. The email can include a link for the user to reset his or her password.
    2. Otherwise, an email is sent to the user to validate the user has control over the email address. It contains a link that the user can click that will validate the email on the server side.
  5. The web application redirects a user to a page indicating that the user should check his/her email to complete registration.
  6. Attacker cries himself to sleep.

There is no information disclosure in this new flow. This flow is trivial to implement and there is a minimal adverse effect on user experience. I find that most web applications follow this pattern.

2. Username Enumeration During Registration

For more critical systems, I highly discourage the use of email addresses as the primary means of identifying a user. Why? Most people have only one or two email addresses and they are easy for attackers to get their hands on. What is the consequence? The attacker no longer has to enumerate users on popular websites, like Amazon.com. If the attacker has a hefty database of email addresses, he can still launch a Password Spraying attack against popular websites with common passwords. Yes, the attacker has a high probability of success.

The Wrong Way

In order to prevent this type of attack, a system must have an alternate method of identifying a user. We refer to this identifier as a username. The problem with usernames boils down to a system allowing a user to select the username. This is a huge problem, because there is no easy way to prevent user enumeration during the registration process. This is outlined in the flow below:

  1. A new user navigates to the web application.
  2. The user doesn’t have an account so s/he proceeds to the registration screen.
  3. The user enters his/her information, including username, and attempts to register.
  4. The web application backend finds that a user with the supplied username already exists. Developers typically have an “Oh, Crap!” moment as there is no obvious way out of this death trap. They choose to return an error, as it is the only logical solution.
  5. The user receives an error message indicating that the email address is already in use.
  6. Attacker does the happy dance.

The Right Way

The vulnerability here entirely rests on the fact that the system allows the user to create a username. There is no risk of duplication if the username is system generated, with one exception. The system must also generate secure, random usernames. If they are dished out sequentially, then all an attacker needs to do is register once and he has enumerated all users. A secure flow is outlined below:

  1. A new user navigates to the web application.
  2. The user doesn’t have an account, so s/he proceeds to the registration screen.
  3. The user enters his/her information and attempts to register.
  4. The web application backend generates a brand new username that cannot be predicted. The system then creates a new user with that username.
  5. The web application returns the username to the user (this can be sent via email).
  6. Attacker experiences a tragic event and suffers from PTSD.

3. Email or Username Enumeration During Authentication

Regardless of whether an email address or username is used to identify a user, I find the next most common information disclosure vulnerability during the authentication process. It is absolutely vital that, if the user enters an incorrect email address, username, or password, the system return identical error messages.

The Wrong Way

I have seen numerous implementations in which either the error messages differed just slightly (perhaps trailing white space, different casing, or a slightly different grammar). I have also seen different HTTP return codes. There should be absolutely no determinable difference in what is returned by the server, regardless of what the user supplies during authentication.

The reason for the different responses is quite simple, and is illustrated in the following flow:

  1. An existing user navigates to the web application.
  2. The user navigates to the login screen.
  3. The user enters an email address and a password and attempts to login.
  4. The web application attempts to find a user with the specified email address. If no user is found, an error is returned to the user.
  5. The web application verifies the supplied password. If the password does not match, an error is returned to the user.
  6. Attacker leaps for joy when error messages don’t match.

The Right Way

I highly recommend using your network inspector to perform a comparison between the two responses. If you aren’t afraid to get your hands dirty, use an interception proxy (such as Burp or Fiddler) and perform a byte-by-byte comparison of the web application’s two responses. The Comparer tab in Burp is quite versatile and easy to use.

4. Email or Username Enumeration During Account Lockout

The next question that I often receive is how to handle account lockout. Since the system can’t indicate the existence of username or email during authentication, what is the best way for the system to communicate lockout?

The Wrong Way, Option 1

I find that this problem rears its ugly head when account lockout is strongly tied to user credentials. If a user lookup fails, the system has no place to store the number of failed login attempts:

  1. User attempts to login with invalid credentials.
  2. System attempts to locate user by username or email address.
    1. If system finds the user, increment the respective user’s invalid login counter.
    2. Otherwise, do nothing.
  3. If the invalid login counter exceeds the configured threshold, notify the user of account lockout.
  4. Attacker goes back to step 1. For nonexistent accounts, attacker never receives an indication of account lockout.

The Wrong Way, Option 2

I have seen developers attempt to solve this problem by tying lockout to session state. This won’t work either, as an attacker can simply launch a new session:

  1. Attacker establishes a new session. System sends attacker a new session cookie.
  2. Attacker attempts to login with invalid credentials.
  3. System attempts to locate user by username or email address.
    1. If the system finds the user, increment the respective user’s invalid login counter.
    2. Otherwise, look in session state for the email/username.
      1. If the session state is found, increment the invalid login counter.
      2. Create session state for the user with an invalid login counter of 1.
    3. If the invalid login counter exceeds the configured threshold, notify the user of account lockout.
  4. Attacker goes back to step 1. For nonexistent accounts, attacker will never receive an indication of account lockout.

The Wrong Way, Option 3

I have also seen developers attempt to solve this problem by tying lockout to IP address. This may seem like a reasonable solution, but it opens the application up to the threat of a coordinated attack. Imagine two attackers trying to determine if user “foo.bar@example.com” exists:

  1. Attacker A attempts to login with “foo.bar@example.com” until he receives an account lockout message.
  2. Seconds later, attacker B attempts to login once with “foo.bar@example.com”.
    1. If he does not receive an account lockout message, then both attackers know that this user does not exist.
    2. Otherwise, then both attackers know that this user does exist.

Note that this also works if a single attacker has access to multiple IP addresses.

The Right Way

In all of the above options, the sole reason for the vulnerability was that account lockout was the implicit child of some other primary record. If account lockout cannot exist without a user, for example, then there is no way to correctly track account lockout for non-existent users.

This also hints at a more fundamental problem: the lack of auditing. If all you are doing is simply incrementing a lockout counter, then you have insufficient logging and monitoring in your system. You won’t be able to adequately piece together and audit trail for authentication attacks against your system.

A better way to handle this is to log every single authentication request (valid or invalid) in a separate table. I know that you database administrators will cringe when I tell you to denormalize this table and remove any foreign key constraints that you have to a user table. Everything should get logged in this table, regardless of whether or not the username or email address exists in your system.

Here’s the flow:

  1. User attempts to login with invalid credentials.
  2. System adds a failed login attempt to the audit table. Add as much contextual information to this audit log, such as IP address, date and time, etc.
  3. System queries the audit table for all failed login attempts since last successful login in a given time frame.
  4. If the number of failed logins exceeds a threshold, notify the user of account lockout.
  5. Attacker is left clueless as to whether or not an account exists.

Conclusion

Managing registration and authentication is an incredibly difficult thing to do securely. If you own a system that must perform authentication, you need to take into account the potential for information disclosure in a myriad of areas. This post only outlined some of the most common sources of information disclosure.

I highly suggest outsourcing authentication. Move the responsibility of registration and authentication out of your system. You can do this with technologies such as OAuth 2.0. If you must implement an authentication system, you may want to consider the Web Authentication (WebAuthn) specification as it radically reduces your attack surface.

 

Photo by Dayne Topkin on Unsplash

Leave a Reply