In Depth: Registration Without Enumeration!

[InDepth#24] It's time to answer the question: how do you build user registration and authentication without an enumeration vector?

In Depth: Registration Without Enumeration!

In last week’s Security Tip: Don't Forget Your Registration Form!, we looked at how registration forms leak user existence and provide a better enumeration vector than password reset forms. Since then, I’ve had a few folks reach out asking about how to build a registration and authentication flow that doesn’t contain an enumeration vector, so let’s dive into it!

The Problem with Enumeration

When you create a new Laravel app1, the default validation rules for the registration page will typically be something like this:

$request->validate([
    'name' => ['required', 'string', 'max:255'],
    'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
    'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);

As you’d expect, the unique rule ensures that the email address the user enters isn’t already in the database. This means that if you provide an email address for an existing user, the validation rule will reject the request and tell you a user account already exists.

Let’s demonstrate this with a new Laravel app2.

I’ve already created a new account with my email address, and when I go back to the registration page and try and submit again, it tells me my email is already being used:

Default Laravel registration form with “The email has already been taken.” validation error.

You can immediately see that I have an account for my email address. You’ll also note I intentionally failed the password confirmation (and used a password that was too short).

If I try a different email address:

Default Laravel registration form with no validation error on a unique email address.

I can immediately identify which of the two email addresses are in the database. Once I know that, I can look at data breaches for that email address, find previously used passwords, and attempt to login.

This is called credential stuffing and is probably the most common method of hijacking user accounts.

Note: Credential stuffing doesn’t rely on knowing the existence of user accounts. An attacker may still attempt to login with known username-password combinations. However multiple login failures for non-existent may flag alerts while multiple registration validation failures may not.

The other aspect of this is vulnerability is user privacy. Some apps may be of a sensitive nature3, where being able to identify if a user has an account may expose some personally revealing or sensitive information. Email addresses are Personally Identifiable Information (PII), after all. In these cases, being able to identify users on the registration form could lead to some serious personally or reputational damage4.

There is also the situation were accounts are created with email addresses of victims, but that isn’t directly related to enumeration, so we won’t be covering it directly.

So how do we stop leaking email addresses?

Rebuilding the Registration Form

To solve this problem, we need to rebuild how the registration form works and remove the unique email address requirement so it stops leaking information.

To do so, we need to decide between a few different options: