From b396952a5cdb249036ebe53c4d45c16107403769 Mon Sep 17 00:00:00 2001 From: Pratyush-Panda-2006 Date: Thu, 9 Jul 2026 21:02:40 +0530 Subject: [PATCH 1/5] fix(backend): implement missing routes and controllers for note reordering and stack grouping (#133) --- backend/src/controllers/notesController.js | 139 ++++++++++++++++++++- backend/src/models/Note.js | 10 +- backend/src/routes/notesRoutes.js | 6 +- 3 files changed, 152 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/notesController.js b/backend/src/controllers/notesController.js index ae50999..d5d978f 100644 --- a/backend/src/controllers/notesController.js +++ b/backend/src/controllers/notesController.js @@ -6,7 +6,7 @@ import Note from "../models/Note.js"; */ export async function getAllNotes(req, res) { try { - const notes = await Note.find({ userId: req.user._id }).sort({ createdAt: -1 }); + const notes = await Note.find({ userId: req.user._id }).sort({ position: 1, createdAt: -1 }); res.status(200).json(notes); } catch (error) { @@ -164,3 +164,140 @@ export async function deleteNote(req, res) { }); } } + +/** + * Reorder notes (drag and drop) + */ +export async function reorderNotes(req, res) { + try { + const { id } = req.params; // Dragged note ID + const { targetId, groupId } = req.body; + + if (!mongoose.Types.ObjectId.isValid(id)) { + return res.status(400).json({ message: "Invalid note id" }); + } + if (targetId && !mongoose.Types.ObjectId.isValid(targetId)) { + return res.status(400).json({ message: "Invalid target id" }); + } + if (groupId && !mongoose.Types.ObjectId.isValid(groupId)) { + return res.status(400).json({ message: "Invalid group id" }); + } + + const draggedNote = await Note.findOne({ _id: id, userId: req.user._id }); + if (!draggedNote) { + return res.status(404).json({ message: "Note not found" }); + } + + const targetParentId = groupId || null; + + // Fetch sibling notes in target container sorted by current position + const siblingNotes = await Note.find({ + userId: req.user._id, + parentId: targetParentId + }).sort({ position: 1, createdAt: -1 }); + + // Filter out the dragged note if it exists in the siblings list + const filteredSiblings = siblingNotes.filter( + (n) => n._id.toString() !== id + ); + + // Find position to insert + if (targetId) { + const targetIndex = filteredSiblings.findIndex( + (n) => n._id.toString() === targetId + ); + if (targetIndex !== -1) { + filteredSiblings.splice(targetIndex, 0, draggedNote); + } else { + filteredSiblings.push(draggedNote); + } + } else { + filteredSiblings.push(draggedNote); + } + + // Prepare bulk write operations to update positions & parentId for all siblings + const bulkOps = filteredSiblings.map((n, index) => ({ + updateOne: { + filter: { _id: n._id }, + update: { $set: { position: index, parentId: targetParentId } } + } + })); + + if (bulkOps.length > 0) { + await Note.bulkWrite(bulkOps); + } + + res.status(200).json({ message: "Notes reordered successfully" }); + } catch (error) { + console.error("Error in reorderNotes controller:", error); + res.status(500).json({ message: "Internal server error" }); + } +} + +/** + * Combine notes into a stack/group + */ +export async function groupNotes(req, res) { + try { + const { sourceId, targetId, title } = req.body; + + if (!sourceId || !targetId || !title || !title.trim()) { + return res.status(400).json({ message: "sourceId, targetId, and title are required" }); + } + + if (!mongoose.Types.ObjectId.isValid(sourceId) || !mongoose.Types.ObjectId.isValid(targetId)) { + return res.status(400).json({ message: "Invalid note IDs" }); + } + + const sourceNote = await Note.findOne({ _id: sourceId, userId: req.user._id }); + const targetNote = await Note.findOne({ _id: targetId, userId: req.user._id }); + + if (!sourceNote || !targetNote) { + return res.status(404).json({ message: "Source or target note not found" }); + } + + // If target is already a group, add the source note to it + if (targetNote.isGroup) { + // Find max position among existing children in this group + const existingChildren = await Note.find({ + userId: req.user._id, + parentId: targetNote._id + }).sort({ position: -1 }); + + const newPosition = existingChildren.length > 0 ? (existingChildren[0].position + 1) : 0; + + sourceNote.parentId = targetNote._id; + sourceNote.position = newPosition; + await sourceNote.save(); + + return res.status(200).json(targetNote); + } + + // Create a new group note + const groupNote = new Note({ + userId: req.user._id, + title: title.trim(), + content: "", + isGroup: true, + parentId: null, + position: targetNote.position || 0 + }); + + const savedGroup = await groupNote.save(); + + // Update source and target notes to point to the new group note + sourceNote.parentId = savedGroup._id; + sourceNote.position = 0; + + targetNote.parentId = savedGroup._id; + targetNote.position = 1; + + await Promise.all([sourceNote.save(), targetNote.save()]); + + res.status(201).json(savedGroup); + } catch (error) { + console.error("Error in groupNotes controller:", error); + res.status(500).json({ message: "Internal server error" }); + } +} + diff --git a/backend/src/models/Note.js b/backend/src/models/Note.js index 7beb664..b9db9b4 100644 --- a/backend/src/models/Note.js +++ b/backend/src/models/Note.js @@ -32,9 +32,17 @@ const noteSchema = new mongoose.Schema({ default: 0 } }, - {timestamps: true}//this will automatically add createdAt and updatedAt fields + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } + } ); +noteSchema.virtual("groupId").get(function () { + return this.parentId; +}); + const Note = mongoose.model("Note", noteSchema); export default Note; \ No newline at end of file diff --git a/backend/src/routes/notesRoutes.js b/backend/src/routes/notesRoutes.js index 1160275..36e6a8f 100644 --- a/backend/src/routes/notesRoutes.js +++ b/backend/src/routes/notesRoutes.js @@ -1,5 +1,5 @@ import express from "express"; -import { getAllNotes, getNoteById, createNote, updateNote, deleteNote } from "../controllers/notesController.js"; +import { getAllNotes, getNoteById, createNote, updateNote, deleteNote, reorderNotes, groupNotes } from "../controllers/notesController.js"; import { authenticateUser } from "../middleware/authMiddleware.js"; const router = express.Router(); @@ -9,6 +9,8 @@ router.use(authenticateUser); router.get("/", getAllNotes); +router.post("/group", groupNotes); + router.get("/:id", getNoteById); router.post("/", createNote); @@ -17,4 +19,6 @@ router.put("/:id", updateNote); router.delete("/:id", deleteNote); +router.patch("/:id/reorder", reorderNotes); + export default router; \ No newline at end of file From 8462513d9af3f6d0a4f458739676a2949c9aacb1 Mon Sep 17 00:00:00 2001 From: Pratyush-Panda-2006 Date: Fri, 31 Jul 2026 08:44:25 +0530 Subject: [PATCH 2/5] fix(backend): sanitize inputs in notesController and add rateLimiter to notesRoutes for CodeQL --- backend/src/controllers/notesController.js | 16 ++++++++-------- backend/src/routes/notesRoutes.js | 4 +++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/notesController.js b/backend/src/controllers/notesController.js index f5eee6a..65852fe 100644 --- a/backend/src/controllers/notesController.js +++ b/backend/src/controllers/notesController.js @@ -183,12 +183,12 @@ export async function reorderNotes(req, res) { return res.status(400).json({ message: "Invalid group id" }); } - const draggedNote = await Note.findOne({ _id: id, userId: req.user._id }); + const draggedNote = await Note.findOne({ _id: new mongoose.Types.ObjectId(id), userId: req.user._id }); if (!draggedNote) { return res.status(404).json({ message: "Note not found" }); } - const targetParentId = groupId || null; + const targetParentId = groupId ? new mongoose.Types.ObjectId(groupId) : null; // Fetch sibling notes in target container sorted by current position const siblingNotes = await Note.find({ @@ -198,13 +198,13 @@ export async function reorderNotes(req, res) { // Filter out the dragged note if it exists in the siblings list const filteredSiblings = siblingNotes.filter( - (n) => n._id.toString() !== id + (n) => n._id.toString() !== String(id) ); // Find position to insert if (targetId) { const targetIndex = filteredSiblings.findIndex( - (n) => n._id.toString() === targetId + (n) => n._id.toString() === String(targetId) ); if (targetIndex !== -1) { filteredSiblings.splice(targetIndex, 0, draggedNote); @@ -241,16 +241,16 @@ export async function groupNotes(req, res) { try { const { sourceId, targetId, title } = req.body; - if (!sourceId || !targetId || !title || !title.trim()) { + if (typeof title !== "string" || !title.trim()) { return res.status(400).json({ message: "sourceId, targetId, and title are required" }); } - if (!mongoose.Types.ObjectId.isValid(sourceId) || !mongoose.Types.ObjectId.isValid(targetId)) { + if (!sourceId || !targetId || !mongoose.Types.ObjectId.isValid(sourceId) || !mongoose.Types.ObjectId.isValid(targetId)) { return res.status(400).json({ message: "Invalid note IDs" }); } - const sourceNote = await Note.findOne({ _id: sourceId, userId: req.user._id }); - const targetNote = await Note.findOne({ _id: targetId, userId: req.user._id }); + const sourceNote = await Note.findOne({ _id: new mongoose.Types.ObjectId(sourceId), userId: req.user._id }); + const targetNote = await Note.findOne({ _id: new mongoose.Types.ObjectId(targetId), userId: req.user._id }); if (!sourceNote || !targetNote) { return res.status(404).json({ message: "Source or target note not found" }); diff --git a/backend/src/routes/notesRoutes.js b/backend/src/routes/notesRoutes.js index 36e6a8f..4e27613 100644 --- a/backend/src/routes/notesRoutes.js +++ b/backend/src/routes/notesRoutes.js @@ -1,10 +1,12 @@ import express from "express"; import { getAllNotes, getNoteById, createNote, updateNote, deleteNote, reorderNotes, groupNotes } from "../controllers/notesController.js"; import { authenticateUser } from "../middleware/authMiddleware.js"; +import rateLimiter from "../middleware/rateLimiter.js"; const router = express.Router(); -// Protect all notes routes - only authenticated users can access notes endpoints +// Protect all notes routes - rate-limit and authenticate +router.use(rateLimiter); router.use(authenticateUser); router.get("/", getAllNotes); From 9838a3fc203c8ee67e202d84bed5427a8c4025d7 Mon Sep 17 00:00:00 2001 From: Pratyush-Panda-2006 Date: Fri, 31 Jul 2026 08:46:11 +0530 Subject: [PATCH 3/5] fix(backend): apply rateLimiter middleware directly to each route handler in notesRoutes --- backend/src/routes/notesRoutes.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/backend/src/routes/notesRoutes.js b/backend/src/routes/notesRoutes.js index 4e27613..1d56c92 100644 --- a/backend/src/routes/notesRoutes.js +++ b/backend/src/routes/notesRoutes.js @@ -5,22 +5,15 @@ import rateLimiter from "../middleware/rateLimiter.js"; const router = express.Router(); -// Protect all notes routes - rate-limit and authenticate -router.use(rateLimiter); +// Protect all notes routes - only authenticated users can access notes endpoints router.use(authenticateUser); -router.get("/", getAllNotes); - -router.post("/group", groupNotes); - -router.get("/:id", getNoteById); - -router.post("/", createNote); - -router.put("/:id", updateNote); - -router.delete("/:id", deleteNote); - -router.patch("/:id/reorder", reorderNotes); +router.get("/", rateLimiter, getAllNotes); +router.post("/group", rateLimiter, groupNotes); +router.get("/:id", rateLimiter, getNoteById); +router.post("/", rateLimiter, createNote); +router.put("/:id", rateLimiter, updateNote); +router.delete("/:id", rateLimiter, deleteNote); +router.patch("/:id/reorder", rateLimiter, reorderNotes); export default router; \ No newline at end of file From edf3ef2f21872a2c2f5ed39cad35c81f4e5fe9f6 Mon Sep 17 00:00:00 2001 From: Pratyush-Panda-2006 Date: Fri, 31 Jul 2026 08:49:15 +0530 Subject: [PATCH 4/5] fix(backend): enhance rateLimiter with express-rate-limit and add CSRF middleware for security checks --- backend/package-lock.json | 52 +++++++++++++++++++++++++++ backend/package.json | 1 + backend/src/middleware/rateLimiter.js | 19 ++++++---- backend/src/server.js | 13 +++++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index c3e022f..0dcefe4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^4.18.2", + "express-rate-limit": "^8.6.1", "jsonwebtoken": "^9.0.3", "mongodb": "^7.2.0", "mongoose": "^8.14.3", @@ -517,6 +518,48 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -727,6 +770,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/backend/package.json b/backend/package.json index 344efe8..effd812 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "cors": "^2.8.5", "dotenv": "^16.5.0", "express": "^4.18.2", + "express-rate-limit": "^8.6.1", "jsonwebtoken": "^9.0.3", "mongodb": "^7.2.0", "mongoose": "^8.14.3", diff --git a/backend/src/middleware/rateLimiter.js b/backend/src/middleware/rateLimiter.js index 7effb36..ed952d7 100644 --- a/backend/src/middleware/rateLimiter.js +++ b/backend/src/middleware/rateLimiter.js @@ -1,8 +1,15 @@ - +import rateLimit from "express-rate-limit"; import ratelimit from "../config/upstash.js"; +const fallbackLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + standardHeaders: true, + legacyHeaders: false, + message: { message: "Too many requests please try after some time" }, +}); + const rateLimiter = async (req, res, next) => { - if (process.env.NODE_ENV !== "production") { return next(); } @@ -10,14 +17,14 @@ const rateLimiter = async (req, res, next) => { try { const ip = req.headers["x-forwarded-for"]?.split(",")[0].trim() || req.socket.remoteAddress || req.ip; const identifier = req.user?._id || ip || "global-rate-limit"; - const { success, limit, remaining, reset } = await ratelimit.limit(identifier); + const { success } = await ratelimit.limit(identifier); if (!success) { return res.status(429).json({ message: "Too many requests please try after some time" }); } - next(); + return next(); } catch (error) { - console.error("Rate limiter error (failing open):", error); - next(); + console.error("Rate limiter error (failing open to fallback):", error); + return fallbackLimiter(req, res, next); } }; diff --git a/backend/src/server.js b/backend/src/server.js index 708a199..83da5f3 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -48,6 +48,19 @@ if (process.env.NODE_ENV !== "production") { app.use(express.json()); app.use(cookieParser()); +// CSRF Protection Middleware +const csrfProtection = (req, res, next) => { + if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method)) { + const origin = req.headers.origin || req.headers.referer; + const allowedOrigin = process.env.CLIENT_URL || "http://localhost:5173"; + if (origin && !origin.startsWith(allowedOrigin)) { + return res.status(403).json({ message: "CSRF check failed: unauthorized origin" }); + } + } + next(); +}; +app.use(csrfProtection); + // Optional auth to populate req.user for rateLimiter const optionalAuthenticateUser = (req, res, next) => { try { From 21d286dd25e354a62b35086b912aa8f7694f91ba Mon Sep 17 00:00:00 2001 From: Pratyush-Panda-2006 Date: Fri, 31 Jul 2026 08:51:50 +0530 Subject: [PATCH 5/5] fix(backend): integrate csurf middleware in server.js for CodeQL CSRF compliance --- backend/package-lock.json | 125 ++++++++++++++++++++++++++++++++++++++ backend/package.json | 1 + backend/src/server.js | 9 ++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 0dcefe4..65b48a5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "bcryptjs": "^3.0.3", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "dotenv": "^16.5.0", "express": "^4.18.2", "express-rate-limit": "^8.6.1", @@ -353,6 +354,94 @@ "node": ">= 0.10" } }, + "node_modules/csrf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", + "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "license": "MIT", + "dependencies": { + "rndm": "1.2.0", + "tsscmp": "1.0.6", + "uid-safe": "2.1.5" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csurf": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", + "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", + "deprecated": "This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions", + "license": "MIT", + "dependencies": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "csrf": "3.1.0", + "http-errors": "~1.7.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/csurf/node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "license": "ISC" + }, + "node_modules/csurf/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/csurf/node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1411,6 +1500,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1448,6 +1546,12 @@ "node": ">=8.10.0" } }, + "node_modules/rndm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", + "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1703,6 +1807,15 @@ "node": ">=18" } }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1716,6 +1829,18 @@ "node": ">= 0.6" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uncrypto": { "version": "0.1.3", "resolved": "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", diff --git a/backend/package.json b/backend/package.json index effd812..1df4adb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,6 +17,7 @@ "bcryptjs": "^3.0.3", "cookie-parser": "^1.4.7", "cors": "^2.8.5", + "csurf": "^1.11.0", "dotenv": "^16.5.0", "express": "^4.18.2", "express-rate-limit": "^8.6.1", diff --git a/backend/src/server.js b/backend/src/server.js index 83da5f3..c234f1b 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -3,6 +3,7 @@ import cors from "cors"; import dotenv from "dotenv"; import path from "path"; import cookieParser from "cookie-parser"; +import csurf from "csurf"; import { fileURLToPath } from "url"; import dns from "dns"; import jwt from "jsonwebtoken"; @@ -49,15 +50,17 @@ app.use(express.json()); app.use(cookieParser()); // CSRF Protection Middleware +const csrfMiddleware = csurf({ cookie: { httpOnly: true, sameSite: "lax" }, ignoreMethods: ["GET", "HEAD", "OPTIONS"] }); const csrfProtection = (req, res, next) => { - if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method)) { + if (process.env.NODE_ENV !== "production" || req.headers.authorization?.startsWith("Bearer ")) { const origin = req.headers.origin || req.headers.referer; const allowedOrigin = process.env.CLIENT_URL || "http://localhost:5173"; - if (origin && !origin.startsWith(allowedOrigin)) { + if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method) && origin && !origin.startsWith(allowedOrigin)) { return res.status(403).json({ message: "CSRF check failed: unauthorized origin" }); } + return next(); } - next(); + return csrfMiddleware(req, res, next); }; app.use(csrfProtection);