Fix email enumeration vulnerabilities in password reset and registration flows
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:
- Password Reset: Previously exposed whether an email existed by returning different error messages for existing vs non-existing accounts
- 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
send_reset_password_instructionsno longer returns errors for non-existent emails by default- Applications relying on explicit "not found" errors will need updates
Changes Made
- Modified
send_reset_password_instructionsinlib/devise/models/recoverable.rb - Updated
PasswordsController#createto always show success - Added paranoid mode handling to
RegistrationsController#create - Created new mailer method and template for "already registered" emails
- Updated locale messages for consistent vague language
- Updated all affected tests
Test Plan
- [x] Unit tests for recoverable model
- [x] Integration tests for password reset flow
- [x] Integration tests for registration flow with paranoid mode
- [x] 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
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.
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.
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.
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.
Also, I could put this behind the paranoid flag.
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.
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.
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.