Bug Description
deserializeUser in backend/config/passportConfig.js calls User.findById(id) without any field projection. This returns the full Mongoose document, including the password field (bcrypt hash), __v, and any future fields added to the schema. The full document is attached to req.user for every authenticated request.
Affected File
backend/config/passportConfig.js lines 24-28
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id); // full document, no projection
done(null, user);
} catch (err) {
done(err, null);
}
});
Why This Is a Problem
Immediate risk: accidental hash exposure
Any route handler that returns req.user to the client (e.g. a future /api/auth/me, a profile endpoint, or an admin panel) will include the password field in the JSON response without the author realising it, because the object looks like a plain user record at the call site.
Offline hash cracking
A leaked bcrypt hash is not plaintext, but it is directly attackable offline. Using a GPU-accelerated cracker and a wordlist, a $2b$10$... hash can be reversed for a significant fraction of users with common passwords. The attacker does not need access to the database; a single leaked API response is enough.
Mongoose document methods on req.user
User.findById() returns a full Mongoose document, not a plain object. This means req.user.comparePassword(), req.user.save(), and other instance methods are callable from route handlers, which is never the intended design for a session user object. It creates a wider attack surface if any route handler incorrectly trusts req.user as user-controlled input and calls document methods on it.
Contrast With the Login Path
The LocalStrategy correctly scopes what gets serialized:
return done(null, {
id: user._id.toString(),
username: user.username,
email: user.email
});
deserializeUser then undoes this careful scoping by returning the full document on every subsequent request.
Steps to Reproduce
- Add a temporary debug route (or inspect
req.user in any existing route handler after login):
app.get('/api/debug/me', (req, res) => {
if (!req.isAuthenticated()) return res.status(401).json({});
res.json(req.user); // will include 'password' field
});
- Log in, then call
GET /api/debug/me.
- Observe that the response includes the
password hash.
Expected Behavior
deserializeUser should exclude sensitive fields so that req.user contains only what is safe to use throughout the application:
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id).select('-password -__v').lean();
done(null, user);
} catch (err) {
done(err, null);
}
});
Using .lean() additionally converts the result to a plain JavaScript object, removing Mongoose document methods from req.user and reducing memory overhead per request.
Severity
High / Security - The password hash is silently present on req.user for every authenticated request. Any route that serializes req.user (current or future) leaks the hash to the client.
This issue is raised under GSSoC 2026 for open-source contribution.
Bug Description
deserializeUserinbackend/config/passportConfig.jscallsUser.findById(id)without any field projection. This returns the full Mongoose document, including thepasswordfield (bcrypt hash),__v, and any future fields added to the schema. The full document is attached toreq.userfor every authenticated request.Affected File
backend/config/passportConfig.jslines 24-28Why This Is a Problem
Immediate risk: accidental hash exposure
Any route handler that returns
req.userto the client (e.g. a future/api/auth/me, a profile endpoint, or an admin panel) will include thepasswordfield in the JSON response without the author realising it, because the object looks like a plain user record at the call site.Offline hash cracking
A leaked bcrypt hash is not plaintext, but it is directly attackable offline. Using a GPU-accelerated cracker and a wordlist, a
$2b$10$...hash can be reversed for a significant fraction of users with common passwords. The attacker does not need access to the database; a single leaked API response is enough.Mongoose document methods on req.user
User.findById()returns a full Mongoose document, not a plain object. This meansreq.user.comparePassword(),req.user.save(), and other instance methods are callable from route handlers, which is never the intended design for a session user object. It creates a wider attack surface if any route handler incorrectly trustsreq.useras user-controlled input and calls document methods on it.Contrast With the Login Path
The LocalStrategy correctly scopes what gets serialized:
deserializeUserthen undoes this careful scoping by returning the full document on every subsequent request.Steps to Reproduce
req.userin any existing route handler after login):GET /api/debug/me.passwordhash.Expected Behavior
deserializeUsershould exclude sensitive fields so thatreq.usercontains only what is safe to use throughout the application:Using
.lean()additionally converts the result to a plain JavaScript object, removing Mongoose document methods fromreq.userand reducing memory overhead per request.Severity
High / Security - The password hash is silently present on
req.userfor every authenticated request. Any route that serializesreq.user(current or future) leaks the hash to the client.This issue is raised under GSSoC 2026 for open-source contribution.