From ea0fd554a414bcea80bab01772b8cf660c8af391 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 15 Aug 2026 05:13:16 +0000 Subject: [PATCH] fix(batches): validate date params + allow-list sort keys in getBatches (#1237) Rebased onto current main. getBatches passed raw start/end query params straight into new Date() (an invalid string becomes Invalid Date and a silent $gte/$lte that matches nothing or everything) and echoed the client-supplied sortBy straight into the Mongoose sort object, letting a client inject arbitrary sort fields. Validate both dates (400 on NaN) and restrict sortBy to an allow-list, defaulting to createdAt. Also removes a stray trailing .catch() block left after the module export that made the file syntactically ambiguous. --- backend/controllers/batchController.js | 38 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/controllers/batchController.js b/backend/controllers/batchController.js index 57d4e66f..105b0bf7 100644 --- a/backend/controllers/batchController.js +++ b/backend/controllers/batchController.js @@ -352,10 +352,26 @@ exports.getBatches = async (req, res) => { if (start || end) { query.createdAt = {}; if (start) { - query.createdAt.$gte = new Date(start); + const startDateObj = new Date(start); + if (Number.isNaN(startDateObj.getTime())) { + return res + .status(400) + .json(apiResponse.errorResponse("Invalid 'start' date. Use ISO 8601 format (e.g. 2024-01-01).")); + } + query.createdAt.$gte = startDateObj; } if (end) { - query.createdAt.$lte = new Date(end); + const endDateObj = new Date(end); + if (Number.isNaN(endDateObj.getTime())) { + return res + .status(400) + .json(apiResponse.errorResponse("Invalid 'end' date. Use ISO 8601 format (e.g. 2024-01-01).")); + } + query.createdAt.$lte = endDateObj; + } + // Drop an empty object if both ends were absent/invalid-but-silenced. + if (!query.createdAt.$gte && !query.createdAt.$lte) { + delete query.createdAt; } } @@ -367,8 +383,21 @@ exports.getBatches = async (req, res) => { ); const skip = (pageNumber - 1) * limitNumber; + // Allow-list sort keys so a client cannot inject arbitrary/malformed + // sort fields into the Mongoose query. + const ALLOWED_SORT_FIELDS = new Set([ + "createdAt", + "updatedAt", + "batchId", + "cropType", + "quantity", + "farmerName", + "status", + "currentStage", + ]); + const safeSortBy = ALLOWED_SORT_FIELDS.has(sortBy) ? sortBy : "createdAt"; const sort = {}; - sort[sortBy] = sortOrder.toLowerCase() === "asc" ? 1 : -1; + sort[safeSortBy] = sortOrder.toLowerCase() === "asc" ? 1 : -1; // Use lean() for read-only queries to skip Mongoose document hydration const batches = await Batch.find(query) @@ -712,6 +741,3 @@ exports.getIoTData = async (req, res) => { ); } }; - -.catch(err => console.error("Promise.all failed:", err)); -};