Skip to content

Commit 009e71c

Browse files
committed
removed password field leakage while populating a collection
1 parent 064d3a6 commit 009e71c

5 files changed

Lines changed: 32 additions & 18 deletions

File tree

‎apps/dashboard-api/src/controllers/auth.controller.js‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ module.exports.login = async (req, res) => {
132132
try {
133133
const { email, password } = loginSchema.parse(req.body);
134134

135-
const dev = await Developer.findOne({ email: email.toLowerCase().trim() });
135+
const dev = await Developer.findOne({ email: email.toLowerCase().trim() }).select('+password');
136136
if (!dev) return res.status(400).json({ error: "User not found" });
137137

138138
const validPass = await bcrypt.compare(password, dev.password);
@@ -156,7 +156,7 @@ module.exports.changePassword = async (req, res) => {
156156
try {
157157
const { currentPassword, newPassword } = changePasswordSchema.parse(req.body);
158158

159-
const dev = await Developer.findById(req.user._id);
159+
const dev = await Developer.findById(req.user._id).select('+password');
160160

161161
const validPass = await bcrypt.compare(currentPassword, dev.password);
162162
if (!validPass) return res.status(400).json({ error: "Incorrect current password" });
@@ -180,7 +180,7 @@ module.exports.deleteAccount = async (req, res) => {
180180
try {
181181
const { password } = deleteAccountSchema.parse(req.body);
182182

183-
const dev = await Developer.findById(req.user._id);
183+
const dev = await Developer.findById(req.user._id).select('+password');
184184

185185
const validPass = await bcrypt.compare(password, dev.password);
186186
if (!validPass) return res.status(400).json({ error: "Incorrect password. Cannot delete account." });
@@ -246,7 +246,7 @@ module.exports.verifyOtp = async (req, res) => {
246246
try {
247247
const { email, otp } = verifyOtpSchema.parse(req.body);
248248

249-
const existingUser = await Developer.findOne({ email });
249+
const existingUser = await Developer.findOne({ email }).select('+password');
250250
if (!existingUser) return res.status(400).json({ error: "User not found" });
251251

252252
const otpDoc = await validateOtp(existingUser._id, otp);
@@ -290,7 +290,7 @@ module.exports.resetPassword = async (req, res) => {
290290
try {
291291
const { email, otp, newPassword } = resetPasswordSchema.parse(req.body);
292292

293-
const dev = await Developer.findOne({ email });
293+
const dev = await Developer.findOne({ email }).select('+password');
294294
if (!dev) return res.status(400).json({ error: "User not found" });
295295

296296
const otpDoc = await validateOtp(dev._id, otp);
@@ -366,7 +366,7 @@ module.exports.refreshToken = async (req, res) => {
366366
}
367367

368368
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET || process.env.JWT_SECRET);
369-
const user = await Developer.findById(decoded._id);
369+
const user = await Developer.findById(decoded._id).select('+refreshToken');
370370

371371
if (!user || user.refreshToken !== refreshToken) {
372372
return res.status(403).json({ error: "Invalid refresh token" });

‎apps/public-api/src/__tests__/userAuth.refresh.test.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ jest.mock('mongoose', () => ({
2222
jest.mock('@urbackend/common', () => {
2323
const z = require('zod');
2424
const mockModel = {
25-
findOne: jest.fn(),
25+
findOne: jest.fn().mockReturnThis(),
2626
create: jest.fn(),
2727
updateOne: jest.fn(),
28+
select: jest.fn().mockReturnThis(),
29+
lean: jest.fn().mockReturnThis(),
2830
};
2931

3032
const projectFindByIdChain = {
@@ -125,7 +127,7 @@ describe('public userAuth refresh flow', () => {
125127
});
126128

127129
test('login issues access token and sets refresh cookie', async () => {
128-
mockModel.findOne.mockResolvedValueOnce({
130+
mockModel.select.mockResolvedValueOnce({
129131
_id: 'user_1',
130132
password: 'hashed_pw',
131133
email: 'a@b.com',

‎apps/public-api/src/controllers/userAuth.controller.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ module.exports.login = async (req, res) => {
10501050
const connection = await getConnection(project._id);
10511051
const Model = getCompiledModel(connection, usersColConfig, project._id, project.resources.db.isExternal);
10521052

1053-
const user = await Model.findOne({ email: normalizedEmail });
1053+
const user = await Model.findOne({ email: normalizedEmail }).select('+password');
10541054

10551055
if (!user) return res.status(400).json({ error: "Invalid email or password" });
10561056

‎packages/common/src/models/Developer.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const developerSchema = new mongoose.Schema({
88
},
99
password: {
1010
type: String,
11-
required: true
11+
required: true,
12+
select: false
1213
},
1314
isVerified: {
1415
type: Boolean,
@@ -20,7 +21,8 @@ const developerSchema = new mongoose.Schema({
2021
},
2122
refreshToken: {
2223
type: String,
23-
default: null
24+
default: null,
25+
select: false
2426
}
2527
}, { timestamps: true });
2628

‎packages/common/src/utils/injectModel.js‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ const typeMapping = {
1010
};
1111

1212
// Recursive field definition builder
13-
function buildFieldDef(field, projectId, isExternal) {
13+
// Recursive field definition builder
14+
function buildFieldDef(field, projectId, isExternal, isUsersCollection = false) {
1415
// Object type — nested sub-schema
1516
if (field.type === "Object" && field.fields && field.fields.length > 0) {
1617
const subSchema = {};
1718
field.fields.forEach((f) => {
1819
const normalizedKey = normalizeKey(f.key);
1920
if (!normalizedKey) return;
2021

21-
subSchema[normalizedKey] = buildFieldDef(f, projectId, isExternal);
22+
subSchema[normalizedKey] = buildFieldDef(f, projectId, isExternal, isUsersCollection);
2223
});
2324
return { type: subSchema, required: !!field.required };
2425
}
@@ -42,7 +43,7 @@ function buildFieldDef(field, projectId, isExternal) {
4243
const normalizedKey = normalizeKey(f.key);
4344
if (!normalizedKey) return;
4445

45-
subSchema[normalizedKey] = buildFieldDef(f, projectId, isExternal);
46+
subSchema[normalizedKey] = buildFieldDef(f, projectId, isExternal, isUsersCollection);
4647
});
4748
return { type: [subSchema], required: !!field.required };
4849
}
@@ -71,25 +72,32 @@ function buildFieldDef(field, projectId, isExternal) {
7172
}
7273

7374
// Primitive types
74-
return {
75+
const def = {
7576
type: typeMapping[field.type],
7677
required: !!field.required,
7778
};
79+
80+
// HARDEN: Exclude password by default for project users
81+
if (isUsersCollection && normalizeKey(field.key) === "password") {
82+
def.select = false;
83+
}
84+
85+
return def;
7886
}
7987
function normalizeKey(key) {
8088
return String(key || "")
8189
.replace(/\uFEFF/g, "")
8290
.trim();
8391
}
8492

85-
function buildMongooseSchema(fieldsArray = [], projectId, isExternal) {
93+
function buildMongooseSchema(fieldsArray = [], projectId, isExternal, isUsersCollection = false) {
8694
const schemaDef = {};
8795

8896
fieldsArray.forEach((field) => {
8997
const normalizedKey = normalizeKey(field.key);
9098
if (!normalizedKey) return;
9199

92-
schemaDef[normalizedKey] = buildFieldDef(field, projectId, isExternal);
100+
schemaDef[normalizedKey] = buildFieldDef(field, projectId, isExternal, isUsersCollection);
93101
});
94102

95103
return new mongoose.Schema(schemaDef, {
@@ -126,7 +134,8 @@ function getCompiledModel(connection, collectionData, projectId, isExternal) {
126134
}
127135

128136
// Build schema + compile
129-
const schema = buildMongooseSchema(collectionData.model, projectId, isExternal);
137+
const isUsersCollection = collectionData.name === "users";
138+
const schema = buildMongooseSchema(collectionData.model, projectId, isExternal, isUsersCollection);
130139
const model = connection.model(collectionName, schema);
131140

132141
// Cache it
@@ -135,6 +144,7 @@ function getCompiledModel(connection, collectionData, projectId, isExternal) {
135144
return model;
136145
}
137146

147+
138148
// Clear cached model (needed when schema changes)
139149
function clearCompiledModel(connection, collectionName) {
140150
if (modelRegistry.has(connection)) {

0 commit comments

Comments
 (0)