Skip to content

Conversation

natevick
Copy link

@natevick natevick commented Aug 14, 2025

Summary

  • Fixes email enumeration vulnerabilities in both password reset and registration flows
  • Makes secure behavior the default for password reset (breaking change)
  • Adds paranoid mode support for registration to prevent email enumeration
  • Updates locale messages to use consistent, vague language that doesn't reveal email existence

Problem

Devise had two email enumeration vulnerabilities:

  1. Password Reset: Previously exposed whether an email existed by returning different error messages for existing vs non-existing accounts
  2. Registration: Showed "Email has already been taken" error when attempting to register with an existing email

Both vulnerabilities allow attackers to enumerate valid user emails.

Solution

Password Reset (Breaking Change - Secure by Default)

  • Changed default behavior to always return success with vague message
  • No longer dependent on paranoid mode for basic security
  • Message: "If your email address exists in our database, you will receive a password recovery link..."

Registration (Opt-in via Paranoid Mode)

  • When Devise.paranoid = true, duplicate registration attempts:
    • Show same success message as new registrations
    • Send "already registered" email to existing user with sign-in/reset links
    • Redirect to sign-in page instead of showing validation errors
  • Default behavior unchanged for backward compatibility

Breaking Changes

  1. send_reset_password_instructions no longer returns errors for non-existent emails by default
  2. Applications relying on explicit "not found" errors will need updates

Changes Made

  1. Modified send_reset_password_instructions in lib/devise/models/recoverable.rb
  2. Updated PasswordsController#create to always show success
  3. Added paranoid mode handling to RegistrationsController#create
  4. Created new mailer method and template for "already registered" emails
  5. Updated locale messages for consistent vague language
  6. Updated all affected tests

Test Plan

  • Unit tests for recoverable model
  • Integration tests for password reset flow
  • Integration tests for registration flow with paranoid mode
  • All tests passing
  • Manual testing of both flows
  • Verify emails sent correctly

Security Impact

This significantly improves security by preventing email enumeration attacks, a common vulnerability used in reconnaissance phases of attacks.

🤖 Generated with Claude Code

Previously, the password reset functionality exposed whether an email
existed in the system by returning different responses for existing vs
non-existing accounts. This is a security vulnerability that allows
attackers to enumerate valid user emails.

This commit makes the following changes:
- Always return a success response with a vague message for password reset requests
- Remove dependency on paranoid mode for this security feature
- Update locale messages to use consistent, non-revealing language
- Update tests to reflect the new secure-by-default behavior

The new behavior always shows: "If your email address exists in our
database, you will receive a password recovery link at your email
address in a few minutes."

This is a breaking change but improves security by default.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@gregmolnar
Copy link

How do you fix the same problem at registration? I usually recommend rate limiting for these, so even if your response indicates the existence of a user, it is difficult to enumerate a large list.

@natevick
Copy link
Author

That is a good question. I would likely need a different solution, which I have not considered yet.

Rate limiting does the trick to stop or make enumeration untenible. It still exposes data unnecessarily, which gets flagged on every pentest, which then has to be dealt with either through explanation or fixing the issue on a per install basis.

@gregmolnar
Copy link

Those pentesters should focus on bigger issues :) I would put such thing maybe as informal on a report and if they don't mention the sign up flow having the same problem, I am not sure what are they doing.

@natevick
Copy link
Author

Yes, they should. I don't disagree, but it has been on multiple reports for multiple companies that I've been involved with over the years.

This PR came out of using a new Saas build on Rails using Devise, so I decided to stop wishing someone open a PR and just did it. I'm happy to work on the sign-up flow as well.

@natevick
Copy link
Author

Also, I could put this behind the paranoid flag.

@gregmolnar
Copy link

Yes, they should. I don't disagree, but it has been on multiple reports for multiple companies that I've been involved with over the years.

This PR came out of using a new Saas build on Rails using Devise, so I decided to stop wishing someone open a PR and just did it. I'm happy to work on the sign-up flow as well.

I am not against this change, I just wanted to point out that this doesn't solve the actual problem, just partially mitigates it.
Other than adding a captcha or rate-limiting to the sign up flow, I am not sure anything else could be done there to prevent this on a scale.
And I know some security companies put these as issues on reports, but I don't think they should(and I am a pentester myself).
With this solution, a timing based attack is still possible as the non existing user will likely return a faster response.

When paranoid mode is enabled, registration attempts with existing
emails now:
- Show the same success message as new registrations
- Send an "already registered" email to the existing user
- Redirect to sign-in page instead of showing validation errors

This prevents attackers from discovering which emails have accounts
by attempting to register with them.

Changes:
- Add paranoid mode check to RegistrationsController#create
- Create new mailer method and template for "already registered" emails
- Update tests to verify the new secure behavior
- Fix test expectations for default non-enumerable password reset

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@natevick natevick changed the title Fix email enumeration vulnerability in password reset Fix email enumeration vulnerabilities in password reset and registration flows Aug 14, 2025
Generate a dummy token when user not found to make timing analysis
harder. This ensures similar computational work is performed whether
the user exists or not, making it more difficult to determine email
existence through response time analysis.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@gregmolnar
Copy link

I have an idea to resolve this during sign up, but I think that would be too big of a change in devise.
The flow would be something like this:
Sign up page with an email field only and once it is submitted, the feedback would be "We will email you a link to sign up" even for existing accounts. For existing accounts the app can decide to send an email asking them to log in or reset their password as they already have an account, for non-existing ones you would send a link to a sign up form with a short-lived token that's verified.

@natevick
Copy link
Author

I like that flow. Could it be behind the paranoid flag? I did something like that on my commits, but changed it in the paranoid flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants