Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth.signUp() doesn't error for existing accounts - security vulnerability #1517

Open
CalebLovell opened this issue Nov 4, 2021 · 46 comments
Assignees
Labels
bug Something isn't working

Comments

@CalebLovell
Copy link

CalebLovell commented Nov 4, 2021

Bug report

Describe the bug

supabase.auth.signUp() is not erroring for existing accounts. Right now, you can submit an existing email with any incorrect password, and supabase will return you the account metadata (without a jwt).

To Reproduce

Go to this example app
Sign-up with an email and a password
Log out
Try to sign up again with the same email using any password you want. Try asdfasdfasdf if you want!
You will get an alert saying you logged in, but you won't get a working access token. Just the email you submitted.
You can also view the request in the Network tab of the Dev Tools and see metadata about the account, like when it was created and what provider it uses.

Expected behavior

Attempting to sign up with an existing email should throw an error.

System information

  • Version of supabase-js: [1.2.1]
  • Version of Node.js: [14.17.4]
@CalebLovell CalebLovell added the bug Something isn't working label Nov 4, 2021
@awalias
Copy link
Member

awalias commented Nov 6, 2021

hey @CalebLovell this is actually a fix for a previous security issue.

Previously the interface was leaking information by allowing an attacker to see whether a given email had an account or not. Now the endpoint returns a "success" response regardless of whether an account exists already or not.

The metadata you see in the response is actually faux info - the user ID is random and the datetimes are set to the time that the request was made.

note: this is only the case for supabase instances where AUTOCONFIRM is disabled (as per the default)
for accounts who have enabled AUTOCONFIRM (where accounts don't require email confirmation) the behavior is the same as before (error on duplicate accounts)

hope this helps!

@awalias awalias closed this as completed Nov 6, 2021
@CalebLovell
Copy link
Author

Ahh, very interesting. Would be great to maybe add that to the docs somewhere, because I suspect I won't be the only one confused by it. Firebase returns an error in this situation so that is what I was expecting, but I like the faux info return as an even better solution! Thanks for the response and enjoy your weekend.

@naegelin
Copy link

This 'security fix' seems like 'security through obscurity' . IMHO it doesn't make sense for supabase to be opinionated about how a signup process should be handled by developers. There are many use cases where a back-end service may need to know if a user already exists and having to store an additional user profile table just to be able to figure this out seems to be an unnecessary extra step. Perhaps the 'service key' responses can be accurate while the 'app key' responds with a generic 'invalid credentials'?

@brunobely
Copy link

In the case where a user forgets they are already signed up with their email address, most websites will show some variation of “there’s already an account for this email address”.

If a user tries to sign up with an email that already has an account associated with it, how can I tell so I can let the user know?

@silentworks
Copy link

silentworks commented Nov 12, 2021

I also agree that security through obscurity is not a good way to fix this as most services online do tell you if a user already exists, what they do have however is a rate limit on how many logins you can try within a certain time period to prevent brute force attacks. This fix should probably be reverted as the behaviour is unexpected and it seems to be confusing users more than anything.

@serranoarevalo
Copy link

@awalias so if the user is trying to create an account, when they already have one, is there a way to let me know?

@ChronSyn
Copy link

ChronSyn commented Nov 16, 2021

I also agree that security through obscurity is not a good way to fix this as most services online do tell you if a user already exists, what they do have however is a rate limit on how many logins you can try within a certain time period to prevent brute force attacks. This fix should probably be reverted as the behaviour is unexpected and it seems to be confusing users more than anything.

Correct. Rate limited logins would be the correct process for this, but it's also difficult to defend against some forms of attack unless they are targeting one specific account, in which case locking that account from logging in for a duration or until an account owner performs some verification process.

If an attacker is aiming to test as many accounts as possible, then the options are either cookies (easily defeated) or IP blocking (also easily defeated). There's some hybrid options out there such as behaviour analysis, but they have their own set of limitations and potential for false positives.

Rate-limiting registration isn't feasible to introduce as an attacker could easily bombard the system with fake registrations and block legitimate users from registering. For ecommerce sites, this would potentially make it a haven for scalpers. At the same time, it becomes difficult for developers when they're unsure if an account exists during registration.

What most online services have is a generic truthy response during the account recovery process. For example, instead of saying "An email was sent to the account we found", they'll instead say "If an account with this email exists, we have sent an email to it". I believe rate limiting per account already exists for this in Supabase.

It's a difficult problem to solve because there's potential for hackers to use the sign up process to ascertain if an account exists (as opposed to testing login), but developers want to know if an account exists when a user is registering.

@naegelin
Copy link

naegelin commented Nov 16, 2021

Every security control can be defeated. Supabase shouldn't be responsible for solving every edge case of security for app developers and many companies have regulatory guidance on how to implement authentication flows. There should be a straight forward api available for server to server calls so that developers can decide what the proper pattern would be for themselves based on their own needs. The current Supabase position is to create an associated "user profile" table inside supabase in order to recreate missing functionality (such as basic checks to see if a user exists). The more of these types of work arounds a developer needs to create, the less compelling supabase becomes over say a vanilla postgres + hasura instance. I would also argue that for B2B applications where access is not available to the general public many of the concerns around account harvesting, fake registration etc are much less of an issue.

@brunobely
Copy link

brunobely commented Nov 22, 2021

@awalias so if the user is trying to create an account, when they already have one, is there a way to let me know?

Building off of this, I think the crux of this issue and what we want to know is what should an average auth flow look like, implementation-wise, when built with Supabase? The only guide available in the official documentation uses magic links "for simplicity" but doesn't really touch on how to handle any error or edge-case scenarios, which is extremely important in production.

I'd just like to know how to do it properly, whichever the security measures taken are.

@awalias
Copy link
Member

awalias commented Nov 23, 2021

Hey everyone thanks for your comments!

To be clear this is not an example of security through obscurity, the red herring here might be the presence of faux info or the fact that we haven't done a good job of documenting this behavior. As long as we don't leak any info about whether a user account exists or not on any public endpoint, then the information is secure (at least as far as this specific issue is concerned).

The correct solution here is to take this one step further: If a user tries to sign up who already has an existing account, we still return a "check your inbox for confirmation email" message, but we instead send them an account recovery email (which is actually just a magic link) then the developer can decide whether to direct them to a password reset page or just allow them to go about their session as normal.

This solves for:

  • UX concerns (a user who has forgotten that they have an account and is expecting a confirmation email, will receive an account recovery email - note: the developer can edit the template in supabase dashboard)
  • DX concerns (there should be no extra call required to detect account existence and trigger account recovery)
  • Security (the presence of an account is never revealed on any public endpoint)

An important reason for why we decided to be "secure by default" on issues like this, is partly because of the number of users adopting our auth service. I could fairly easily take your email address, make a sign-up request to every known supabase endpoint on the internet, building up a map of all the various services that you are/are-not subscribed to. Which is quite a large privacy concern.

I propose that we do several things:

As always any help on these ones would be hugely appreciated. And thanks again for the active discussion and helping improve the product 🙏.

@awalias
Copy link
Member

awalias commented Nov 23, 2021

@naegelin I also agree with your idea that the admin endpoints should return truthful responses, if you are creating an account from the backend. We recently added auth.api.createUser I'm not sure what the behavior of this method is for existing accounts.

edit: it looks like this already works as expected - again the issue here is lack of documentation

{
  data: null,
  error: {
    message: 'Email address already registered by another user',
    status: 422
  }
}

@akkie
Copy link

akkie commented Feb 18, 2022

@awalias Is it still true that the signUp function will not throw an error if the user was already registered? I ask because I get currently the error 'User already registered' if I register with an already existing email address.

My config says:
enable_signup = true
double_confirm_changes = true
enable_confirmations = true

@kevinmlong
Copy link

kevinmlong commented Aug 21, 2022

@awalias - There still seems to be a mismatch here that I'm trying to work through. My account is set-up to require email confirmations.

  • If I try to signUp a user using the email address for an existing user that has not been confirmed, it returns the obfuscated user, and an additional "Confirm Your Signup" email is sent to the email address
  • If I try to signUp a user using the email address for an existing user that has already been confirmed, it returns the obfuscated user, but no email is sent to the user

Based on what you mentioned above, it would seem that a confirmation/recovery email should be sent in both case and in the UI, one can simply direct the user to check their email. That said, I'm not sure how to handle the case where a user already signed up using that email without them getting confused on the what the password is. In the case of a confirmed user getting an account recovery email, their mental model would be the password they just entered, which would be wrong if it's different than the one actually associated with the account.

@irreal
Copy link

irreal commented Oct 31, 2022

We are hitting the exact same issue. Registered users with verified email addresses can forget they already signed up, they go through the sign up process, we show the message to check their emails, but no email arrives.

Any solutions?

@malewis5
Copy link

Is there any update to this? We need a clear solution for letting users know if they've already signed up, like how @awalias mentioned. I can see many users getting stuck and waiting for a sign up email that will never come.

@awalias
Copy link
Member

awalias commented Nov 18, 2022

hey! Was just discussing this issue with @hf in depth - his opinion is that since it's still possible for people to determine the existence of accounts with side-channel attacks (e.g. time how long it takes to get a response, if an email is sent it will take longer etc.) that by default we should return an error similar to how firebase does it. e.g. "This email already exists".

Then the idea is to make this functionality optional to people who require more privacy controls. There is some draft documentation on the issue here: https://github.com/supabase/supabase/blob/f8682bbb095d728ee19a581170a5d972a39543e4/apps/reference/docs/guides/auth/auth-security.mdx#user-information

In the meantime I will check if we can revert the default behavior to always give an error for existing accounts

@awalias awalias reopened this Nov 18, 2022
@renardeinside
Copy link

Adding a bit more color to this issue.
The message Check your email for the confirmation link. pops up if a signUp email exists in the users table, but is added from a different provider, e.g. GitHub. This is kind of misleading (from one side), but also secure from another side.

I believe this behavior should be documented to avoid others losing a couple of hours trying to debug it.

@farhanhaider1
Copy link

Is there any update to this? We need a clear solution for letting users know if they've already signed up, like how @awalias mentioned. I can see many users getting stuck and waiting for a sign up email that will never come.

exactly this issue!

@user72356
Copy link

@awalias Any updates on this issue? The UX is very poor without a way of letting them know that they already have an account. And if I have to pre-vet the user, then that's a poor DX.

From https://github.com/supabase/supabase/blob/f8682bbb095d728ee19a581170a5d972a39543e4/apps/reference/docs/guides/auth/auth-security.mdx#user-information

"information about whether an account exists in the system can leak even if the application returns an ambigouous message such as "If you have an account an email has been sent." This is because the final part of the request handling will need to send out an email message or SMS using a third-party system. This makes requests naturally last longer when an account exists."

I don't understand this- couldn't this email or SMS process happen asynchronously so that you can return the response immediately in all cases?

Alternatively, your proposed fix of sending it whether there is an account or not means it will always take the same (long) time.

@anarkrypto
Copy link

It makes sense to maintain privacy, but it doesn't make sense not to email existing users.

Let's assume that my user registered with Github.

If he loses access to github or wants to create a password, he will be waiting for the email forever and will think that not receiving it is an error.

This is very confusing. User should be able to confirm their new form of authentication via email

@hugonteifeh
Copy link

hugonteifeh commented Mar 18, 2023

Complete protection against user enumeration seems hard to achieve with email-based logins. This is why I consider username-based logins to be an easy and practical solution to this problem. It'd be much simpler to ask users to choose a username/login that doesn't identify them, than to ask them to create an anonymous email/phone number and come back and use this "anonymous" email.

Users would still need to provide their emails/SMS upon registration(to support OTP, magic links, etc...) but it'd only ask for the username upon signing in. Yes, this means that supabase would still need to have some mechanism to prevent leakage of the existence of an email if the system is to have a functionality like "Forget username? send it to email..." but I imagine that protection against such a leakage would be easy to implement by always responding in a constant time.

I noticed that someone asked for this feature #903 a while ago but it was closed. I think the supabase team should reconsider this feature.

Privacy is a superhigh priority and should always be approached with an opt-out model rather than an opt-in model.

@kvetoslavnovak
Copy link

I have just discovered this behavior today and found this issue, really surprised and agree that This 'security fix' seems like 'security through obscurity'.

@ghost
Copy link

ghost commented Jun 5, 2023

I've just opened this issue https://github.com/supabase/gotrue-js/issues/769, which is related to the discussion going on here.

@devhandler
Copy link

it would be really helpful if you can let developers decide how to handle this instead of forcing a solution. it's always a balance between security and convenience and nothing is absolutely secure. each service has different needs and developers can choose what works the best for their use cases

@arshamg
Copy link

arshamg commented Jun 12, 2023

In case this helps anyone, we don't verify emails in our dev/staging environment but we do verify emails in production. Due to this, we have a signUp function that looks like:

export const signUp = async (email: string, password: string) => {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  });

  // in staging, we don't verify primary emails
  // Supabase returns a nice error
  if (error?.message === "User already registered")
    return { data, error: "Please sign in with your existing account" };

  if (error) {
    logger(error.message, "error");
    throw new Error(error.message);
  }

  // in production, we verify primary emails
  // supabase returns a user object with no identities if the user exists
  if (data?.user?.identities?.length === 0) {
    return { data, error: "Please sign in with your existing account" };
  }

  return { data, error };
};

@wmonecke
Copy link

wmonecke commented Aug 10, 2023

How is this hack:

const {error, data} = await supabase.auth.signUp({email, password})
if(data?.user?.identities?.length === 0){
    alert("This user already exists")
}

still the acceptable solution? A hacker could also read this thread and exploit the hack to see if the user exists. This means that the "security" argument is not valid.

It's almost over half a year and this issue seems stale.

@devhandler
Copy link

@awalias @kiwicopple hope supabase leave the freedom to developers to decide how to handle existing accounts instead of dictating a solution that is really not helpful for a lot of use cases

@Saranoja
Copy link

Saranoja commented Oct 9, 2023

Hey guys, here's an interesting finding → it appears that if you're running signUp from the server side / from a serverless function (using the service key), the returned error actually contains the message "User already registered" as opposed to running is anonymously - case in which, yes, a fake user is returned. So a reliable alternative would be to just create a custom Edge function and have that invoked. Then the function can handle the scenario however you want (send an email, forward the error etc.).

@nmerchant
Copy link

@Saranoja thank you!! That's super helpful to know.

@aftermidnightsolutions
Copy link

aftermidnightsolutions commented Dec 23, 2023

Keep it simple...check for an error coming back from signUp.

If there was an error, something went wrong (password not right length, etc.) so display that.

if there was no error, 1 of 2 things happened.

  1. user already existed
  2. user was created

If you to check the length of the return to figure out which, ok that's fine.

I chose to just return a generic message either way...something like:

"New users require authentication. Please check your email. If you are already a user and forgot your password, please reset your password form the login form"

With that approach, you don't tip your hat as to if the user existed or not, and you tip an existing user to try reseting their password if they forgot.

I suppose on condition 1 above, where the user already exists, if you get a zero length result, you could also programmatically initiate an email indicating to the user someone tried signing up using their email address. While supabase may not do this for you, there is no stoping you from doing it in the code based on the return.

@TomasSestak
Copy link

It's been more than two years 👀. It happens all the time that users forget that they registered before...

@danieldietzel
Copy link

Same problem, this is an incredibly goofy decision to not return a basic error. At least make it configurable to turn the error on and off.

@aftermidnightsolutions
Copy link

aftermidnightsolutions commented Jan 18, 2024

Same problem, this is an incredibly goofy decision to not return a basic error. At least make it configurable to turn the error on and off.

I'm seeing it does return an error. When using the following:

const { error } = await supabase.auth.signUp({ email, password })

error.message will contain "user already registered" as the error message, if the user already exists.

@842u
Copy link

842u commented Jan 18, 2024

Adding to the complexity, when email confirmation is enabled, using a service won't throw an error for an existing confirmed email. However, if you self-host supabase manually or via CLI, it will throw an error stating 'User already registered".

@SEI-John
Copy link

SEI-John commented Apr 5, 2024

@awalias, would it be possible to get an update on where supabase stands on this issue? The behaviour is a little strange to say the least, and it seems like there isn't a firm stance ~3 years after this has been first reported. I love the package so far, but there's the odd bug/strange behaviour here and there that would be great to get resolved. Hacking around this by checking the identities array isn't optimal.

@kiptoomm
Copy link

As mentioned here: supabase/auth-js#513 (comment), it seems to me that the latest docs have clarified the approach:

If signUp() is called for an existing confirmed user:
When both Confirm email and Confirm phone (even when phone provider is disabled) are enabled in your project, an obfuscated/fake user object is returned.
When either Confirm email or Confirm phone (even when phone provider is disabled) is disabled, the error message, User already registered is returned.

It's still not an optimal solution, but at least we can get an explicit error message by disabling the email confirmation. The error response looks like:

{"code":422,"error_code":"user_already_exists","msg":"User already registered"}

@RowinVanAmsterdam
Copy link

I do not get the magic link in the mailbox when I try to sign up with an already existing user? I only get the fake user data object in return.

Email service is working, signup emails and sending magic link manually works without a problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests