Authentication mechanisms should not reveal whether a username exists in the system.

Why is this an issue?

User enumeration occurs when an application discloses whether a given username exists in its database, for example through sign-in, sign-on, or forgot-password functionalities. When error messages or exception handling differ depending on whether a username is valid, attackers can use brute-force techniques to harvest valid usernames. This facilitates further attacks such as credential stuffing, phishing, and targeted password guessing, and impacts the privacy of the affected users.

What is the potential impact?

If an attacker can enumerate valid usernames, it significantly increases the success rate of credential stuffing or social engineering. They can launch targeted credential-stuffing or password-guessing campaigns against confirmed accounts, and use the harvested usernames in phishing schemes. This also degrades user privacy, since the mere existence of an account can reveal personal information.

How to fix it in Spring

Code examples

The following code leaks information about the existence of usernames by using distinct error messages, throwing UsernameNotFoundException outside the loadUserByUsername method, or disabling HideUserNotFoundExceptions.

Noncompliant code example

public String authenticate(String username, String password) {

  MyUserDetailsService s1 = new MyUserDetailsService();
  MyUserPrincipal u1 = s1.loadUserByUsername(username);

  if(u1 == null) {
    throw new BadCredentialsException(username+" doesn't exist in our database"); // Noncompliant
  }

}

public String authenticate2(String username, String password) {

  MyUserDetailsService s2 = new MyUserDetailsService();
  MyUserPrincipal user = s2.loadUserByUsername(username);

  if(user == null) {
      throw new UsernameNotFoundException("user not found"); // Noncompliant
  }

}

public void configure() {
  DaoAuthenticationProvider daoauth = new DaoAuthenticationProvider();
  daoauth.setUserDetailsService(new MyUserDetailsService());
  daoauth.setPasswordEncoder(new BCryptPasswordEncoder());
  daoauth.setHideUserNotFoundExceptions(false); // Noncompliant
  builder.authenticationProvider(daoauth);
}

Compliant solution

public boolean authenticate(String username, String password) throws AuthenticationException {
  verifyCredentials(username, password);
  return true;
}

private void verifyCredentials(String username, String password) throws AuthenticationException {
  Details user = null;
  try {
    user = loadUserByUsername(username);
  } catch (UsernameNotFoundException | DataAccessException e) {
    // Hide the reason to avoid disclosing user existence.
  }
  if (user == null || !user.isPasswordCorrect(password)) {
    throw new BadCredentialsException("Bad credentials");
  }
}

public void configure() {
  DaoAuthenticationProvider daoauth = new DaoAuthenticationProvider();
  daoauth.setUserDetailsService(new MyUserDetailsService());
  daoauth.setPasswordEncoder(new BCryptPasswordEncoder());
  daoauth.setHideUserNotFoundExceptions(true);
  builder.authenticationProvider(daoauth);
}

Resources

Documentation

Standards