Skip to content

fix(security): auth enumeration + prod error message leak#3790

Merged
PierreBrisorgueil merged 2 commits into
masterfrom
fix/auth-enum-error-leak
Jun 4, 2026
Merged

fix(security): auth enumeration + prod error message leak#3790
PierreBrisorgueil merged 2 commits into
masterfrom
fix/auth-enum-error-leak

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • [audit][P1][security] auth: forgot endpoint allows account enumeration + timing oracle #3722 — auth: account enumeration + timing oracle (modules/auth/controllers/auth.password.controller.js)

    • forgot endpoint now returns a single uniform success response ("If that email exists, a reset link has been sent.") for all paths: unknown email, OAuth user, and known local user — no longer leaks whether an address is registered
    • Unknown/OAuth path performs a dummy bcrypt.compare against a precomputed valid 60-char bcrypt hash (cost=10) so the full KDF runs and equalises response timing
  • [audit][P1][security] core: initErrorRoutes leaks err.message/code to clients in production #3726 — core: initErrorRoutes leaks err.message/code in prod (lib/services/express.js)

    • Added NODE_ENV === 'production' guard in the global error handler: returns { message: 'Internal Server Error' } in prod (no err.message/code); detailed body preserved in non-prod envs for debuggability
  • Tests: 4 new unit tests for auth.password.controller.forgot + 3 new unit tests for express.initErrorRoutes; all 1731 unit tests green, lint clean

Test plan

  • npm run test:unit — 1731 tests, 126 suites, all green
  • npm run lint — no issues
  • Manual: POST /api/auth/password/forgot with unknown email → same 200 response as known email, no enumeration signal
  • Manual: trigger global error handler in production mode → response body is { message: 'Internal Server Error' }, not raw error detail

Closes #3722
Closes #3726

Summary by CodeRabbit

  • Bug Fixes

    • Improved security handling of error messages in production environments to prevent exposing sensitive internal system details.
    • Enhanced password reset functionality with additional security measures to prevent account enumeration attacks.
  • Tests

    • Added regression tests for production error handling security.
    • Added regression tests for password reset account enumeration prevention.

- forgot endpoint: uniform response for known/unknown/OAuth email paths
  + dummy bcrypt.compare on unknown-user path to equalise timing
- initErrorRoutes: sanitise err.message/code in production (return
  generic 'Internal Server Error'); detailed body kept in non-prod
- Unit tests: 4 for auth.password.controller + 3 for express.errorroutes

Closes #3722
Closes #3726
Copilot AI review requested due to automatic review settings June 4, 2026 14:44
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4ec91d7-2b6b-4c03-8494-0657a9b7b55f

📥 Commits

Reviewing files that changed from the base of the PR and between d1e93a3 and 559140f.

📒 Files selected for processing (5)
  • lib/services/express.js
  • lib/services/tests/express.errorroutes.unit.tests.js
  • modules/auth/controllers/auth.password.controller.js
  • modules/auth/tests/auth.integration.tests.js
  • modules/auth/tests/auth.password.controller.unit.tests.js

Walkthrough

This PR hardens two security findings from audit #3726 and #3722. The Express error handler now filters internal details in production environments, returning only a generic message. The auth forgot endpoint eliminates account enumeration and timing oracles by always returning identical success responses and executing a constant-time dummy hash for unknown emails.

Changes

Security Hardening: Error Handler and Auth Enumeration Fixes

Layer / File(s) Summary
Express error handler production sanitization
lib/services/express.js, lib/services/tests/express.errorroutes.unit.tests.js
Error handler detects NODE_ENV === 'production' and returns generic { message: 'Internal Server Error' } instead of leaking err.message and err.code. Non-production environments retain original error details. Tests verify response behavior across production, development, and test environments, including absence of raw error substrings in production responses.
Auth forgot endpoint enumeration and timing protection
modules/auth/controllers/auth.password.controller.js, modules/auth/tests/auth.password.controller.unit.tests.js
Forgot endpoint now returns uniform success response for all email states (unknown, OAuth user, or known local user) and executes constant-time dummy comparePassword when user not found to equalize response timing. Token generation and reset email logic remains unchanged for local users. Tests validate response uniformity for unknown emails, that dummy hash path executes, that known local users receive uniform success, and that OAuth provider names do not leak.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pierreb-devkit/Node#3268: Modifies modules/auth/controllers/auth.password.controller.js to harden against auth enumeration and input manipulation patterns.
  • pierreb-devkit/Node#3442: Modifies lib/services/express.js error-handling middleware logging behavior, directly intertwined with this PR's production response sanitization changes.

Suggested labels

Fix, Tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(security): auth enumeration + prod error message leak' accurately summarizes the two main security fixes: addressing auth enumeration in the forgot endpoint and preventing error message leaks in production.
Description check ✅ Passed The PR description covers all required template sections: Summary with detailed context, linked issues (Closes #3722, #3726), Scope section, test plan with concrete validation steps, and Guardrails check coverage.
Linked Issues check ✅ Passed All coding requirements from issues #3722 and #3726 are implemented: uniform forgot-password response with timing-oracle mitigation via dummy bcrypt compare, and production-only error message sanitization in the global error handler.
Out of Scope Changes check ✅ Passed All changes directly address the linked security issues: password controller enumeration fix, Express error handler production guard, and corresponding test suites for regression prevention. No extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/auth-enum-error-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.23%. Comparing base (cdacb9b) to head (559140f).
⚠️ Report is 6 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3790      +/-   ##
==========================================
+ Coverage   90.19%   90.23%   +0.03%     
==========================================
  Files         152      152              
  Lines        5020     5026       +6     
  Branches     1598     1599       +1     
==========================================
+ Hits         4528     4535       +7     
+ Misses        387      386       -1     
  Partials      105      105              
Flag Coverage Δ
integration 59.09% <75.00%> (+0.02%) ⬆️
unit 69.63% <91.66%> (+0.29%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 853b36b...559140f. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses two audit findings by (1) hardening the auth “forgot password” flow against account/provider enumeration and timing signals, and (2) preventing the global Express error handler from leaking internal error details to clients in production.

Changes:

  • Auth: forgot now returns a uniform success message for unknown emails and non-local (OAuth) users and runs a dummy bcrypt compare on those paths to reduce timing leakage.
  • Core: initErrorRoutes now returns a generic { message: 'Internal Server Error' } body in NODE_ENV=production, while keeping detailed error bodies in non-prod.
  • Tests: adds new unit coverage for the auth forgot handler and for the production/non-production behavior of initErrorRoutes.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
modules/auth/controllers/auth.password.controller.js Makes forgot return a uniform success response for unknown/OAuth users and adds a dummy bcrypt compare to reduce timing signals.
modules/auth/tests/auth.password.controller.unit.tests.js Adds unit tests guarding against enumeration and verifying the dummy bcrypt path.
lib/services/express.js Adds a production-only guard to avoid leaking err.message/err.code to clients.
lib/services/tests/express.errorroutes.unit.tests.js Adds unit tests verifying prod returns a generic error body and non-prod keeps detailed fields.

Comment on lines +28 to +30
// Uniform success message — never reveal whether an email is registered (anti-enumeration).
const uniformSuccess = () => responses.success(res, 'If that email exists, a reset link has been sent.')({ status: true });

Comment on lines 77 to 81
if (!mail || !mail.accepted) return responses.error(res, 400, 'Bad Request', 'Failure sending email')();
return responses.success(res, 'An email has been sent with further instructions')({ status: true });
return uniformSuccess();
} catch (_mailErr) {
return responses.error(res, 400, 'Bad Request', 'Failure sending email')();
}
Comment on lines +94 to +95
// Must call responses.success (uniform positive signal)
expect(responses.success).toHaveBeenCalled();

await PasswordController.forgot(req, res);

expect(responses.success).toHaveBeenCalled();
Comment on lines +164 to +165
// Must call responses.success (uniform positive signal)
expect(responses.success).toHaveBeenCalled();
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/auth/controllers/auth.password.controller.js (1)

27-82: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update /api/auth/forgot integration-test expectations for uniform success

  • In modules/auth/tests/auth.integration.tests.js, change the two “should return 400” cases for (1) non-existent email and (2) non-local provider to .expect(200) and assert result.body.message === 'If that email exists, a reset link has been sent.' (and result.body.data.status === true).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/auth/controllers/auth.password.controller.js` around lines 27 - 82,
Update the two integration tests that currently expect 400 for the "non-existent
email" and "non-local provider" cases in auth.integration.tests.js to expect 200
instead, and add assertions that result.body.message === 'If that email exists,
a reset link has been sent.' and result.body.data.status === true; locate the
tests by their descriptions ("should return 400" for non-existent email and for
OAuth/non-local provider) and change .expect(400) to .expect(200) and add the
two equality checks on result.body.message and result.body.data.status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/services/express.js`:
- Around line 326-333: Add a JSDoc header above the express error-handling
middleware callback to satisfy the JS guideline: include a one-line description
and `@param` entries for (err, req, res, next) with types (e.g., Error, Request,
Response, NextFunction) and an `@returns` describing the possible return (e.g.,
{Response|void} or express Response when sending an error); apply this to the
anonymous middleware function defined in app.use((err, req, res, next) => { ...
}) so the modified function has the required JSDoc contract.

In `@lib/services/tests/express.errorroutes.unit.tests.js`:
- Around line 21-24: Add a complete JSDoc header above the named async helper
getInitErrorRoutes describing its purpose in one line and include an `@returns`
tag documenting the resolved value (the extracted initErrorRoutes function
wrapped in a Promise); for example document it as returning Promise<Function>
(or a more specific function type if available) so callers know the async helper
resolves to the initErrorRoutes callable.

In `@modules/auth/controllers/auth.password.controller.js`:
- Around line 77-81: The current branch in auth.password.controller.js returns
responses.error(...) when mail sending fails (the if-check using mail.accepted
and the catch block catching _mailErr), which re-introduces account enumeration;
change both failure paths to return uniformSuccess() instead, and record the
underlying error server-side via the existing logging mechanism (e.g., use
processLogger.error or console.error) to log _mailErr and any mail response
details for ops. Ensure you update the if block that currently does "if (!mail
|| !mail.accepted) return responses.error(...)" and the catch that returns
responses.error(...) so both return uniformSuccess() while still logging the
failure.

In `@modules/auth/tests/auth.password.controller.unit.tests.js`:
- Around line 115-167: Add a unit test in
modules/auth/tests/auth.password.controller.unit.tests.js that simulates
mail-send failure and asserts the controller still returns the uniform success:
arrange a user with provider 'local', set mockGetBrut to resolve that user, set
mockUpdate to resolve the updated user, and set mockSendMail to simulate failure
(either mockResolvedValueOnce({ accepted: [] }) or mockRejectedValueOnce(...));
import PasswordController and responses, call PasswordController.forgot(req,
res) with the user's email, then assert responses.success was called and
responses.error was not called (and/or no call contains provider info),
referencing the existing mocks mockSendMail, mockGetBrut, mockUpdate and the
PasswordController.forgot and responses.success/error symbols.

---

Outside diff comments:
In `@modules/auth/controllers/auth.password.controller.js`:
- Around line 27-82: Update the two integration tests that currently expect 400
for the "non-existent email" and "non-local provider" cases in
auth.integration.tests.js to expect 200 instead, and add assertions that
result.body.message === 'If that email exists, a reset link has been sent.' and
result.body.data.status === true; locate the tests by their descriptions
("should return 400" for non-existent email and for OAuth/non-local provider)
and change .expect(400) to .expect(200) and add the two equality checks on
result.body.message and result.body.data.status.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4be0a2ee-8c08-400a-a07b-caf9515c7928

📥 Commits

Reviewing files that changed from the base of the PR and between 853b36b and d1e93a3.

📒 Files selected for processing (4)
  • lib/services/express.js
  • lib/services/tests/express.errorroutes.unit.tests.js
  • modules/auth/controllers/auth.password.controller.js
  • modules/auth/tests/auth.password.controller.unit.tests.js

Comment thread lib/services/express.js
Comment thread lib/services/tests/express.errorroutes.unit.tests.js
Comment thread modules/auth/controllers/auth.password.controller.js Outdated
Comment thread modules/auth/tests/auth.password.controller.unit.tests.js
…eration tests

- auth.password.controller: mail failure now logs + returns uniformSuccess()
  instead of 400, closing enumeration oracle on SMTP errors (#3722)
- auth.integration.tests: update all forgot-flow assertions to uniform 200
  (unknown email, OAuth provider, mail failure, password-reset setup calls)
- auth.password.controller.unit.tests: strengthen to exact UNIFORM_MSG;
  add tests for sendMail throws and sendMail !accepted paths
- express.js initErrorRoutes: add JSDoc on function and 4-arity handler (#3726)
- express.errorroutes.unit.tests: add @returns to getInitErrorRoutes JSDoc
@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PierreBrisorgueil PierreBrisorgueil dismissed coderabbitai[bot]’s stale review June 4, 2026 15:38

Stale review from pre-fix commit; all 4 actionable comments addressed in 559140f, CI green + Snyk pass.

@PierreBrisorgueil PierreBrisorgueil merged commit 428fb18 into master Jun 4, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/auth-enum-error-leak branch June 4, 2026 15:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants