From 7986d10e3334620e4f9788d770fc367a9b618e6a Mon Sep 17 00:00:00 2001 From: Vexx Date: Fri, 22 May 2026 01:03:57 +0530 Subject: [PATCH] Fix session fixation and password hash exposure in login flow 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 #375 --- backend/config/passportConfig.js | 9 +++++---- backend/routes/auth.js | 26 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50ca..b19ee9b3 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -9,12 +9,13 @@ passport.use( try { const user = await User.findOne( {email} ); if (!user) { - return done(null, false, { message: 'Email is invalid '}); + // Generic message prevents user enumeration + return done(null, false, { message: 'Invalid credentials' }); } const isMatch = await user.comparePassword(password); if (!isMatch) { - return done(null, false, { message: 'Invalid password' }); + return done(null, false, { message: 'Invalid credentials' }); } return done(null, { @@ -34,10 +35,10 @@ passport.serializeUser((user, done) => { done(null, user.id); }); -// Deserialize user (retrieve user from session) +// Deserialize user — exclude password hash from req.user on every request passport.deserializeUser(async (id, done) => { try { - const user = await User.findById(id); + const user = await User.findById(id).select('-password'); done(null, user); } catch (err) { done(err, null); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..7e6381cf 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -26,13 +26,29 @@ router.post("/signup", validateRequest(signupSchema), async (req, res) => { return res.status(400).json({ message: 'User already exists' }); } - res.status(500).json({ message: 'Error creating user', error: err.message }); + res.status(500).json({ message: 'Error creating user' }); } }); -// Login route -router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { - res.status(200).json( { message: 'Login successful', user: req.user } ); +// Login route — session is regenerated after successful authentication +// to prevent session fixation; only safe fields returned in the response +router.post("/login", validateRequest(loginSchema), (req, res, next) => { + passport.authenticate('local', (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(401).json({ message: info?.message || 'Invalid credentials' }); + + req.session.regenerate((regenerateErr) => { + if (regenerateErr) return next(regenerateErr); + + req.logIn(user, (loginErr) => { + if (loginErr) return next(loginErr); + res.status(200).json({ + message: 'Login successful', + user: { id: user.id, username: user.username, email: user.email }, + }); + }); + }); + })(req, res, next); }); // Logout route @@ -41,7 +57,7 @@ router.get("/logout", (req, res) => { req.logout((err) => { if (err) - return res.status(500).json({ message: 'Logout failed', error: err.message }); + return res.status(500).json({ message: 'Logout failed' }); else res.status(200).json({ message: 'Logged out successfully' }); });