From b387f09149cfa589f99ab3e7d45f0c213ea1af37 Mon Sep 17 00:00:00 2001 From: Milind Gupta Date: Sat, 22 Aug 2026 12:53:22 +0530 Subject: [PATCH 1/2] fix: prevent account enumeration on recover endpoint for invalid email domains (#2702) --- internal/api/recover.go | 8 ++++++-- internal/api/recover_test.go | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/api/recover.go b/internal/api/recover.go index 7c967aaf00..ad77bc9a23 100644 --- a/internal/api/recover.go +++ b/internal/api/recover.go @@ -69,8 +69,12 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { return a.sendPasswordRecovery(r, tx, user, flowType) }) if err != nil { - return err + // Prevent account enumeration: if sending password recovery fails because the email address + // is invalid or rejected by the mailer, return HTTP 200 OK to match the non-existent user path. + if httpErr, ok := err.(*apierrors.HTTPError); ok && httpErr.ErrorCode == apierrors.ErrorCodeEmailAddressInvalid { + return sendJSON(w, http.StatusOK, map[string]string{}) + } } return sendJSON(w, http.StatusOK, map[string]string{}) -} +} \ No newline at end of file diff --git a/internal/api/recover_test.go b/internal/api/recover_test.go index a7e655c596..2d56ca013a 100644 --- a/internal/api/recover_test.go +++ b/internal/api/recover_test.go @@ -151,3 +151,24 @@ func (ts *RecoverTestSuite) TestRecover_NoSideChannelLeak() { ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusOK, w.Code) } + +func (ts *RecoverTestSuite) TestRecover_EmailAddressInvalidNoLeak() { + email := "test@example.com" + + // Request body + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "email": email, + })) + + // Setup request + req := httptest.NewRequest(http.MethodPost, "http://localhost/recover", &buffer) + req.Header.Set("Content-Type", "application/json") + + // Setup response recorder + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + + // Both existing and missing users should return 200 OK without leaking account existence + assert.Equal(ts.T(), http.StatusOK, w.Code) +} \ No newline at end of file From 0869fe42451da9824b0f8514ddeeb9d81e29b3dc Mon Sep 17 00:00:00 2001 From: Milind Gupta Date: Sat, 22 Aug 2026 13:21:21 +0530 Subject: [PATCH 2/2] fix: preserve non-email_address_invalid error returns in recover --- internal/api/recover.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/api/recover.go b/internal/api/recover.go index ad77bc9a23..824b4f5432 100644 --- a/internal/api/recover.go +++ b/internal/api/recover.go @@ -74,6 +74,8 @@ func (a *API) Recover(w http.ResponseWriter, r *http.Request) error { if httpErr, ok := err.(*apierrors.HTTPError); ok && httpErr.ErrorCode == apierrors.ErrorCodeEmailAddressInvalid { return sendJSON(w, http.StatusOK, map[string]string{}) } + // Return all other errors (rate limits 429, DB transaction errors 500, etc.) + return err } return sendJSON(w, http.StatusOK, map[string]string{})