From 1cbcc41036c6e0f3bbae15219ac67274f591fd19 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 18 Mar 2026 09:13:59 +0100 Subject: [PATCH 1/3] fix(security): harden auth endpoints against NoSQL injection and add JWT warning --- config/index.js | 1 + lib/helpers/config.js | 7 +++++++ modules/auth/controllers/auth.controller.js | 4 ++++ modules/auth/controllers/auth.password.controller.js | 8 +++++--- modules/users/repositories/users.repository.js | 9 ++++++++- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/config/index.js b/config/index.js index c7640e194..e7442ea0b 100644 --- a/config/index.js +++ b/config/index.js @@ -135,6 +135,7 @@ const initGlobalConfig = async () => { configHelper.initSecureMode(config); // Print a warning if config.domain is not set if (process.env.NODE_ENV !== 'test') configHelper.validateDomainIsSet(config); + if (process.env.NODE_ENV !== 'test') configHelper.validateJwtSecret(config); // Expose configuration utilities const conf = { ...config }; conf.utils = { diff --git a/lib/helpers/config.js b/lib/helpers/config.js index 2d3721d98..f14ee467e 100644 --- a/lib/helpers/config.js +++ b/lib/helpers/config.js @@ -54,6 +54,12 @@ const validateDomainIsSet = (config) => { } }; +const validateJwtSecret = (config) => { + if (config.jwt && config.jwt.secret === 'WaosSecretKeyExampleToChnageAbsolutely') { + console.log(chalk.red('+ Important warning: JWT secret is set to the default value. It should be changed in production via DEVKIT_NODE_jwt_secret.')); + } +}; + /** * Validate secure parameters and create SSL credentials. * @param {object} config - application configuration object @@ -106,6 +112,7 @@ const initGlobalConfigFiles = async (assets) => { export default { getGlobbedPaths, validateDomainIsSet, + validateJwtSecret, initSecureMode, initGlobalConfigFiles, }; diff --git a/modules/auth/controllers/auth.controller.js b/modules/auth/controllers/auth.controller.js index c6ededc40..b00304ef8 100644 --- a/modules/auth/controllers/auth.controller.js +++ b/modules/auth/controllers/auth.controller.js @@ -322,6 +322,10 @@ const oauthCallback = async (req, res, next) => { const strategy = req.params.strategy; // app Auth with Strategy managed on client side if (req.body.strategy === false && req.body.key) { + const allowedKeys = ['id', 'sub', 'email']; + if (!allowedKeys.includes(req.body.key)) { + return responses.error(res, 422, 'Unprocessable Entity', 'Invalid provider key')(); + } try { let user = { firstName: req.body.firstName, diff --git a/modules/auth/controllers/auth.password.controller.js b/modules/auth/controllers/auth.password.controller.js index 78f1daf7e..a64d2fdcb 100644 --- a/modules/auth/controllers/auth.password.controller.js +++ b/modules/auth/controllers/auth.password.controller.js @@ -27,9 +27,10 @@ const forgot = async (req, res) => { let user; // check input if (!req.body.email) return responses.error(res, 422, 'Unprocessable Entity', 'Mail field must not be blank')(); + const email = String(req.body.email); // get user generate and add token try { - user = await UserService.getBrut({ email: req.body.email }); + user = await UserService.getBrut({ email }); if (!user) return responses.error(res, 400, 'Bad Request', 'No account with that email has been found')(); if (user.provider !== 'local') return responses.error(res, 400, 'Bad Request', `It seems like you signed up using your ${user.provider} account`)(); @@ -68,7 +69,7 @@ const forgot = async (req, res) => { */ const validateResetToken = async (req, res) => { try { - const user = await UserService.getBrut({ resetPasswordToken: req.params.token }); + const user = await UserService.getBrut({ resetPasswordToken: String(req.params.token) }); if (!user || !user.email) return res.redirect('/api/password/reset/invalid'); res.redirect(`/api/password/reset/${req.params.token}`); } catch (_err) { @@ -86,9 +87,10 @@ const reset = async (req, res) => { let user; // check input if (!req.body.token || !req.body.newPassword) return responses.error(res, 400, 'Bad Request', 'Password or Token fields must not be blank')(); + const token = String(req.body.token); // get user by token, update with new password, login again try { - user = await UserService.getBrut({ resetPasswordToken: req.body.token }); + user = await UserService.getBrut({ resetPasswordToken: token }); if (!user || !user.email) return responses.error(res, 400, 'Bad Request', 'Password reset token is invalid or has expired.')(); const edit = { password: await AuthService.hashPassword(req.body.newPassword), diff --git a/modules/users/repositories/users.repository.js b/modules/users/repositories/users.repository.js index c5415582b..4abfe8526 100644 --- a/modules/users/repositories/users.repository.js +++ b/modules/users/repositories/users.repository.js @@ -76,7 +76,14 @@ const get = (user = {}) => { * @param {Object} mongoose input request * @returns {Array} users */ -const search = (input) => User.find(input).exec(); +const search = (input) => { + for (const value of Object.values(input)) { + if (typeof value === 'object' && value !== null) { + throw new Error('Invalid search parameter'); + } + } + return User.find(input).exec(); +}; /** * @desc Function to update a user in db From f86176d86f3f5bccbbe07308c42620a5a59d0fb1 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 18 Mar 2026 09:25:16 +0100 Subject: [PATCH 2/3] fix(users): remove overly aggressive search validation that broke internal queries The repository-level object check rejected all object values including legitimate MongoDB operators ($in, etc.) used by seed and internal code. NoSQL injection is already prevented at controller boundaries via String() coercion and key allowlists. --- modules/users/repositories/users.repository.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/modules/users/repositories/users.repository.js b/modules/users/repositories/users.repository.js index 4abfe8526..c5415582b 100644 --- a/modules/users/repositories/users.repository.js +++ b/modules/users/repositories/users.repository.js @@ -76,14 +76,7 @@ const get = (user = {}) => { * @param {Object} mongoose input request * @returns {Array} users */ -const search = (input) => { - for (const value of Object.values(input)) { - if (typeof value === 'object' && value !== null) { - throw new Error('Invalid search parameter'); - } - } - return User.find(input).exec(); -}; +const search = (input) => User.find(input).exec(); /** * @desc Function to update a user in db From 1af7297889b2c0e34d015526984c0a763492a950 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 18 Mar 2026 09:30:34 +0100 Subject: [PATCH 3/3] fix(config): add JSDoc to validateJwtSecret helper --- lib/helpers/config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/helpers/config.js b/lib/helpers/config.js index f14ee467e..32f804218 100644 --- a/lib/helpers/config.js +++ b/lib/helpers/config.js @@ -54,6 +54,11 @@ const validateDomainIsSet = (config) => { } }; +/** + * @desc Warn if JWT secret is still set to the default placeholder value. + * @param {object} config - application configuration object + * @returns {void} + */ const validateJwtSecret = (config) => { if (config.jwt && config.jwt.secret === 'WaosSecretKeyExampleToChnageAbsolutely') { console.log(chalk.red('+ Important warning: JWT secret is set to the default value. It should be changed in production via DEVKIT_NODE_jwt_secret.'));