Skip to content

Fix session fixation vulnerability and password hash exposure in login#378

Merged
mehul-m-prajapati merged 1 commit into
GitMetricsLab:mainfrom
advikdivekar:security/session-fixation-password-hash
May 23, 2026
Merged

Fix session fixation vulnerability and password hash exposure in login#378
mehul-m-prajapati merged 1 commit into
GitMetricsLab:mainfrom
advikdivekar:security/session-fixation-password-hash

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What is the problem

Two vulnerabilities compound in the authentication flow:

Session fixation: passport.authenticate('local') was used as direct middleware, which stores the authenticated user into the existing pre-login session ID without regenerating it. An attacker who can plant a known session ID in the victim's browser (via subdomain cookie injection or briefly shared access) becomes fully authenticated as that victim after the victim logs in — no password required.

Password hash in login response: deserializeUser called User.findById(id) with no field projection, loading the full Mongoose document including the bcrypt-hashed password into req.user. The login route then returned user: req.user directly, sending the hash over the wire to every client that logs in. This hash can be cracked offline with a dictionary.

A third issue fixed in the same pass: distinct error messages ('Email is invalid' vs 'Invalid password') in the Passport strategy allowed an attacker to enumerate which email addresses are registered.

What was changed

backend/routes/auth.js

  • Refactored login from passport.authenticate('local') as middleware to the callback form, enabling explicit error handling and flow control.
  • Calls req.session.regenerate() before req.logIn() so the server issues a new session ID on every successful login, destroying the pre-login session server-side.
  • Returns only { id, username, email } in the response body — never req.user which contains the full document.
  • Removed error: err.message from the signup and logout 500 responses to prevent leaking internal server details.

backend/config/passportConfig.js

  • Added .select('-password') to the User.findById() call in deserializeUser so the password hash is never loaded into memory on any authenticated request.
  • Replaced 'Email is invalid' and 'Invalid password' with a single generic 'Invalid credentials' message to close the user-enumeration vector.

Why this approach

Session regeneration must happen before req.logIn(), not after. After regenerate() the old session is destroyed server-side and req.session points to a new empty session object; logIn() then writes the user ID into that new session. Reversing the order would leave a window where the old session holds the authenticated state. The callback form of passport.authenticate is required because middleware form gives no hook between authentication success and the response, making it impossible to call regenerate() in the correct position.

Using .select('-password') at the database layer (rather than deleting from the returned object) ensures the hash is never allocated in memory at all and cannot leak through any future code path that reads req.user.

How to test

  1. Make any unauthenticated request (e.g., GET /api/auth/logout) and record the connect.sid cookie value from the response.
  2. POST /api/auth/login with valid credentials. Observe the Set-Cookie header — connect.sid must be a different value from step 1.
  3. Inspect the JSON response body — it must contain exactly { "message": "Login successful", "user": { "id": "...", "username": "...", "email": "..." } } with no password field at any level.
  4. POST /api/auth/login with a registered email and wrong password, then with an unregistered email. Both must return { "message": "Invalid credentials" } with status 401 — identical responses.

Edge cases covered

  • regenerateErr and loginErr are both forwarded to next() so they surface as 500 responses rather than silent hangs or unhandled promise rejections.
  • info?.message uses optional chaining so a missing info object from Passport does not throw.
  • .select('-password') is applied at query level, not as a post-processing delete, so the hash cannot be exposed by any future spread or serialisation of req.user.

Verification

  • Root cause fully resolved
  • All edge cases handled
  • No regressions introduced — login, logout, and signup routes continue to work; existing test assertions for status codes and response shape still pass
  • Code matches project style and conventions

Labels: type:security level:advanced gssoc:approved

Closes #375

Please assign this PR to me under GSSoC 2026.

Regenerate session ID after successful login to prevent session fixation
attacks. Refactor login to callback form of passport.authenticate so
req.session.regenerate() can be called before req.logIn().

Return only safe fields (id, username, email) in the login response
instead of the full req.user document which included the bcrypt hash.

Fix deserializeUser to use .select('-password') so the hash is never
loaded into req.user on subsequent authenticated requests.

Unify auth failure messages to 'Invalid credentials' in the Passport
strategy to prevent user enumeration via distinct error strings.

Strip err.message from 500 error responses in signup and logout
to prevent leaking internal server details to clients.

Closes GitMetricsLab#375
@netlify
Copy link
Copy Markdown

netlify Bot commented May 21, 2026

Deploy Preview for github-spy ready!

Name Link
🔨 Latest commit 7986d10
🔍 Latest deploy log https://app.netlify.com/projects/github-spy/deploys/6a0f5ef140197c00087ae3eb
😎 Deploy Preview https://deploy-preview-378--github-spy.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Warning

Rate limit exceeded

@advikdivekar has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 51 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29229659-436e-4819-b116-92285bf66cb5

📥 Commits

Reviewing files that changed from the base of the PR and between 9d34c19 and 7986d10.

📒 Files selected for processing (2)
  • backend/config/passportConfig.js
  • backend/routes/auth.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@mehul-m-prajapati mehul-m-prajapati merged commit 17134e4 into GitMetricsLab:main May 23, 2026
7 checks passed
@github-actions
Copy link
Copy Markdown

🎉🎉 Thank you for your contribution! Your PR #378 has been merged! 🎉🎉

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Session Fixation Vulnerability and bcrypt Password Hash Leaked in Login Response

2 participants