From ee2cfcd20b5b9260571976b15c48f7e910e8412e Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sat, 13 Jun 2026 11:44:48 +0530 Subject: [PATCH 1/4] fix(scheduled): enforce XSS sanitization & Cloudinary validation for scheduled messages --- .../scheduledMessage.controller.js | 43 ++++++++++----- backend/src/lib/attachmentValidator.js | 52 +++++++++++++++++++ backend/src/lib/messageScheduler.js | 44 ++++++++++++++-- 3 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 backend/src/lib/attachmentValidator.js diff --git a/backend/src/controllers/scheduledMessage.controller.js b/backend/src/controllers/scheduledMessage.controller.js index 3cd3fc1..406dd54 100644 --- a/backend/src/controllers/scheduledMessage.controller.js +++ b/backend/src/controllers/scheduledMessage.controller.js @@ -1,6 +1,9 @@ import mongoose from "mongoose"; import ScheduledMessage from "../models/scheduledMessage.model.js"; import User from "../models/user.model.js"; +import xss from "xss"; +import cloudinary from "../lib/cloudinary.js"; +import { validateImageAttachment, validateAudioAttachment } from "../lib/attachmentValidator.js"; // ── POST /messages/schedule ────────────────────────────────────── /** @@ -13,47 +16,63 @@ export async function scheduleMessage(req, res) { const { receiverId, message, image, audio, scheduledFor, replyTo } = req.body; const senderId = req.userId; - // Validation + // Validation of receiver if (!receiverId || !mongoose.Types.ObjectId.isValid(receiverId)) { return res.status(400).json({ message: "Valid receiver ID is required." }); } - + // Validate scheduled time if (!scheduledFor) { return res.status(400).json({ message: "Scheduled timestamp is required." }); } - const scheduledDate = new Date(scheduledFor); const now = new Date(); - if (scheduledDate <= now) { return res.status(400).json({ message: "Scheduled time must be in the future." }); } - + // At least one content if (!message && !image && !audio) { return res.status(400).json({ message: "Message, image, or audio is required." }); } - // Check receiver exists const receiver = await User.findById(receiverId); if (!receiver) { return res.status(404).json({ message: "Receiver user not found." }); } - + // Sanitize message text if present + const sanitizedMessage = message ? xss(message.trim()) : ""; + // Validate and upload image + let imageUrl = ""; + if (image) { + const validation = validateImageAttachment(image); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(image); + imageUrl = result.secure_url; + } + // Validate and upload audio + let audioUrl = ""; + if (audio) { + const validation = validateAudioAttachment(audio); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(audio, { resource_type: "auto" }); + audioUrl = result.secure_url; + } // Create scheduled message const scheduledMessage = new ScheduledMessage({ senderId, receiverId, - message: message || "", - image: image || "", - audio: audio || "", + message: sanitizedMessage, + image: imageUrl, + audio: audioUrl, scheduledFor: scheduledDate, replyTo: replyTo || null, status: "pending", }); - await scheduledMessage.save(); await scheduledMessage.populate("senderId receiverId", "name profilePicture"); - res.status(201).json({ message: "Message scheduled successfully.", data: scheduledMessage, diff --git a/backend/src/lib/attachmentValidator.js b/backend/src/lib/attachmentValidator.js new file mode 100644 index 0000000..4d2a6ec --- /dev/null +++ b/backend/src/lib/attachmentValidator.js @@ -0,0 +1,52 @@ +/* + * Attachment validation utilities shared between live message handling and scheduled messages. + * These functions validate Base64 data URLs for images and audio files, enforce MIME allow‑lists, + * and ensure size limits (5 MB for images, 10 MB for audio). They return an object + * { isValid: boolean, error?: string }. + */ + +export const validateImageAttachment = (base64Str, maxSizeBytes = 5 * 1024 * 1024) => { + // Verify Data URL format + const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); + if (!match) { + return { isValid: false, error: "Invalid file format structure or corrupt payload." }; + } + const mimeType = match[1]; + const rawData = match[2]; + const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; + if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { + return { isValid: false, error: "Unsupported image signature type. Allowed formats: JPEG, PNG, WEBP, GIF." }; + } + // Estimate binary size + const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); + if (binarySizeEstimate > maxSizeBytes) { + return { isValid: false, error: "File boundary limit exceeded. Image size must be under 5MB." }; + } + return { isValid: true }; +}; + +export const validateAudioAttachment = (base64Str, maxSizeBytes = 10 * 1024 * 1024) => { + const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); + if (!match) { + return { isValid: false, error: "Invalid audio format structure or corrupt payload." }; + } + const mimeType = match[1]; + const rawData = match[2]; + const ALLOWED_MIME_TYPES = [ + "audio/webm", + "audio/mp3", + "audio/wav", + "audio/mpeg", + "audio/ogg", + "audio/x-m4a", + "audio/m4a", + ]; + if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { + return { isValid: false, error: "Unsupported audio format. Allowed formats: WEBM, MP3, WAV, OGG, M4A." }; + } + const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); + if (binarySizeEstimate > maxSizeBytes) { + return { isValid: false, error: "Audio size exceeds the 10MB limit." }; + } + return { isValid: true }; +}; diff --git a/backend/src/lib/messageScheduler.js b/backend/src/lib/messageScheduler.js index f60231c..a8fd62a 100644 --- a/backend/src/lib/messageScheduler.js +++ b/backend/src/lib/messageScheduler.js @@ -2,6 +2,9 @@ import ScheduledMessage from "../models/scheduledMessage.model.js"; import Message from "../models/message.model.js"; import { io, getReceiverSocketIds } from "./socket.js"; import webpush from "./webpush.js"; +import xss from "xss"; +import cloudinary from "./cloudinary.js"; +import { validateImageAttachment, validateAudioAttachment } from "./attachmentValidator.js"; let schedulerInterval = null; @@ -13,13 +16,48 @@ const SCHEDULER_INTERVAL = 30000; // Check every 30 seconds */ async function processScheduledMessage(scheduledMsg) { try { + // Sanitize message text + const sanitizedMessage = scheduledMsg.message ? xss(scheduledMsg.message.trim()) : ""; + + // Handle image attachment + let imageUrl = ""; + if (scheduledMsg.image) { + if (scheduledMsg.image.startsWith('data:')) { + const validation = validateImageAttachment(scheduledMsg.image); + if (!validation.isValid) { + console.error(`[Scheduler] Image validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + return; // Skip processing this message + } + const uploadResult = await cloudinary.uploader.upload(scheduledMsg.image); + imageUrl = uploadResult.secure_url; + } else { + imageUrl = scheduledMsg.image; // Assume already a URL + } + } + + // Handle audio attachment + let audioUrl = ""; + if (scheduledMsg.audio) { + if (scheduledMsg.audio.startsWith('data:')) { + const validation = validateAudioAttachment(scheduledMsg.audio); + if (!validation.isValid) { + console.error(`[Scheduler] Audio validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + return; // Skip processing this message + } + const uploadResult = await cloudinary.uploader.upload(scheduledMsg.audio, { resource_type: "auto" }); + audioUrl = uploadResult.secure_url; + } else { + audioUrl = scheduledMsg.audio; // Assume already a URL + } + } + // Create the actual message const newMessage = new Message({ senderId: scheduledMsg.senderId, receiverId: scheduledMsg.receiverId, - message: scheduledMsg.message, - image: scheduledMsg.image, - audio: scheduledMsg.audio, + message: sanitizedMessage, + image: imageUrl, + audio: audioUrl, replyTo: scheduledMsg.replyTo, status: "sent", }); From c9859182eb423df790ae0b73f7149a4180a3c406 Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sat, 13 Jun 2026 11:50:38 +0530 Subject: [PATCH 2/4] Revert "fix(scheduled): enforce XSS sanitization & Cloudinary validation for scheduled messages" This reverts commit ee2cfcd20b5b9260571976b15c48f7e910e8412e. --- .../scheduledMessage.controller.js | 43 +++++---------- backend/src/lib/attachmentValidator.js | 52 ------------------- backend/src/lib/messageScheduler.js | 44 ++-------------- 3 files changed, 15 insertions(+), 124 deletions(-) delete mode 100644 backend/src/lib/attachmentValidator.js diff --git a/backend/src/controllers/scheduledMessage.controller.js b/backend/src/controllers/scheduledMessage.controller.js index 406dd54..3cd3fc1 100644 --- a/backend/src/controllers/scheduledMessage.controller.js +++ b/backend/src/controllers/scheduledMessage.controller.js @@ -1,9 +1,6 @@ import mongoose from "mongoose"; import ScheduledMessage from "../models/scheduledMessage.model.js"; import User from "../models/user.model.js"; -import xss from "xss"; -import cloudinary from "../lib/cloudinary.js"; -import { validateImageAttachment, validateAudioAttachment } from "../lib/attachmentValidator.js"; // ── POST /messages/schedule ────────────────────────────────────── /** @@ -16,63 +13,47 @@ export async function scheduleMessage(req, res) { const { receiverId, message, image, audio, scheduledFor, replyTo } = req.body; const senderId = req.userId; - // Validation of receiver + // Validation if (!receiverId || !mongoose.Types.ObjectId.isValid(receiverId)) { return res.status(400).json({ message: "Valid receiver ID is required." }); } - // Validate scheduled time + if (!scheduledFor) { return res.status(400).json({ message: "Scheduled timestamp is required." }); } + const scheduledDate = new Date(scheduledFor); const now = new Date(); + if (scheduledDate <= now) { return res.status(400).json({ message: "Scheduled time must be in the future." }); } - // At least one content + if (!message && !image && !audio) { return res.status(400).json({ message: "Message, image, or audio is required." }); } + // Check receiver exists const receiver = await User.findById(receiverId); if (!receiver) { return res.status(404).json({ message: "Receiver user not found." }); } - // Sanitize message text if present - const sanitizedMessage = message ? xss(message.trim()) : ""; - // Validate and upload image - let imageUrl = ""; - if (image) { - const validation = validateImageAttachment(image); - if (!validation.isValid) { - return res.status(400).json({ message: validation.error }); - } - const result = await cloudinary.uploader.upload(image); - imageUrl = result.secure_url; - } - // Validate and upload audio - let audioUrl = ""; - if (audio) { - const validation = validateAudioAttachment(audio); - if (!validation.isValid) { - return res.status(400).json({ message: validation.error }); - } - const result = await cloudinary.uploader.upload(audio, { resource_type: "auto" }); - audioUrl = result.secure_url; - } + // Create scheduled message const scheduledMessage = new ScheduledMessage({ senderId, receiverId, - message: sanitizedMessage, - image: imageUrl, - audio: audioUrl, + message: message || "", + image: image || "", + audio: audio || "", scheduledFor: scheduledDate, replyTo: replyTo || null, status: "pending", }); + await scheduledMessage.save(); await scheduledMessage.populate("senderId receiverId", "name profilePicture"); + res.status(201).json({ message: "Message scheduled successfully.", data: scheduledMessage, diff --git a/backend/src/lib/attachmentValidator.js b/backend/src/lib/attachmentValidator.js deleted file mode 100644 index 4d2a6ec..0000000 --- a/backend/src/lib/attachmentValidator.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Attachment validation utilities shared between live message handling and scheduled messages. - * These functions validate Base64 data URLs for images and audio files, enforce MIME allow‑lists, - * and ensure size limits (5 MB for images, 10 MB for audio). They return an object - * { isValid: boolean, error?: string }. - */ - -export const validateImageAttachment = (base64Str, maxSizeBytes = 5 * 1024 * 1024) => { - // Verify Data URL format - const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); - if (!match) { - return { isValid: false, error: "Invalid file format structure or corrupt payload." }; - } - const mimeType = match[1]; - const rawData = match[2]; - const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; - if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { - return { isValid: false, error: "Unsupported image signature type. Allowed formats: JPEG, PNG, WEBP, GIF." }; - } - // Estimate binary size - const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); - if (binarySizeEstimate > maxSizeBytes) { - return { isValid: false, error: "File boundary limit exceeded. Image size must be under 5MB." }; - } - return { isValid: true }; -}; - -export const validateAudioAttachment = (base64Str, maxSizeBytes = 10 * 1024 * 1024) => { - const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); - if (!match) { - return { isValid: false, error: "Invalid audio format structure or corrupt payload." }; - } - const mimeType = match[1]; - const rawData = match[2]; - const ALLOWED_MIME_TYPES = [ - "audio/webm", - "audio/mp3", - "audio/wav", - "audio/mpeg", - "audio/ogg", - "audio/x-m4a", - "audio/m4a", - ]; - if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { - return { isValid: false, error: "Unsupported audio format. Allowed formats: WEBM, MP3, WAV, OGG, M4A." }; - } - const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); - if (binarySizeEstimate > maxSizeBytes) { - return { isValid: false, error: "Audio size exceeds the 10MB limit." }; - } - return { isValid: true }; -}; diff --git a/backend/src/lib/messageScheduler.js b/backend/src/lib/messageScheduler.js index a8fd62a..f60231c 100644 --- a/backend/src/lib/messageScheduler.js +++ b/backend/src/lib/messageScheduler.js @@ -2,9 +2,6 @@ import ScheduledMessage from "../models/scheduledMessage.model.js"; import Message from "../models/message.model.js"; import { io, getReceiverSocketIds } from "./socket.js"; import webpush from "./webpush.js"; -import xss from "xss"; -import cloudinary from "./cloudinary.js"; -import { validateImageAttachment, validateAudioAttachment } from "./attachmentValidator.js"; let schedulerInterval = null; @@ -16,48 +13,13 @@ const SCHEDULER_INTERVAL = 30000; // Check every 30 seconds */ async function processScheduledMessage(scheduledMsg) { try { - // Sanitize message text - const sanitizedMessage = scheduledMsg.message ? xss(scheduledMsg.message.trim()) : ""; - - // Handle image attachment - let imageUrl = ""; - if (scheduledMsg.image) { - if (scheduledMsg.image.startsWith('data:')) { - const validation = validateImageAttachment(scheduledMsg.image); - if (!validation.isValid) { - console.error(`[Scheduler] Image validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); - return; // Skip processing this message - } - const uploadResult = await cloudinary.uploader.upload(scheduledMsg.image); - imageUrl = uploadResult.secure_url; - } else { - imageUrl = scheduledMsg.image; // Assume already a URL - } - } - - // Handle audio attachment - let audioUrl = ""; - if (scheduledMsg.audio) { - if (scheduledMsg.audio.startsWith('data:')) { - const validation = validateAudioAttachment(scheduledMsg.audio); - if (!validation.isValid) { - console.error(`[Scheduler] Audio validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); - return; // Skip processing this message - } - const uploadResult = await cloudinary.uploader.upload(scheduledMsg.audio, { resource_type: "auto" }); - audioUrl = uploadResult.secure_url; - } else { - audioUrl = scheduledMsg.audio; // Assume already a URL - } - } - // Create the actual message const newMessage = new Message({ senderId: scheduledMsg.senderId, receiverId: scheduledMsg.receiverId, - message: sanitizedMessage, - image: imageUrl, - audio: audioUrl, + message: scheduledMsg.message, + image: scheduledMsg.image, + audio: scheduledMsg.audio, replyTo: scheduledMsg.replyTo, status: "sent", }); From 18c8049a7b5030899e97454f33550da808b81ded Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sat, 13 Jun 2026 11:54:06 +0530 Subject: [PATCH 3/4] Reapply "fix(scheduled): enforce XSS sanitization & Cloudinary validation for scheduled messages" This reverts commit c9859182eb423df790ae0b73f7149a4180a3c406. --- .../scheduledMessage.controller.js | 43 ++++++++++----- backend/src/lib/attachmentValidator.js | 52 +++++++++++++++++++ backend/src/lib/messageScheduler.js | 44 ++++++++++++++-- 3 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 backend/src/lib/attachmentValidator.js diff --git a/backend/src/controllers/scheduledMessage.controller.js b/backend/src/controllers/scheduledMessage.controller.js index 3cd3fc1..406dd54 100644 --- a/backend/src/controllers/scheduledMessage.controller.js +++ b/backend/src/controllers/scheduledMessage.controller.js @@ -1,6 +1,9 @@ import mongoose from "mongoose"; import ScheduledMessage from "../models/scheduledMessage.model.js"; import User from "../models/user.model.js"; +import xss from "xss"; +import cloudinary from "../lib/cloudinary.js"; +import { validateImageAttachment, validateAudioAttachment } from "../lib/attachmentValidator.js"; // ── POST /messages/schedule ────────────────────────────────────── /** @@ -13,47 +16,63 @@ export async function scheduleMessage(req, res) { const { receiverId, message, image, audio, scheduledFor, replyTo } = req.body; const senderId = req.userId; - // Validation + // Validation of receiver if (!receiverId || !mongoose.Types.ObjectId.isValid(receiverId)) { return res.status(400).json({ message: "Valid receiver ID is required." }); } - + // Validate scheduled time if (!scheduledFor) { return res.status(400).json({ message: "Scheduled timestamp is required." }); } - const scheduledDate = new Date(scheduledFor); const now = new Date(); - if (scheduledDate <= now) { return res.status(400).json({ message: "Scheduled time must be in the future." }); } - + // At least one content if (!message && !image && !audio) { return res.status(400).json({ message: "Message, image, or audio is required." }); } - // Check receiver exists const receiver = await User.findById(receiverId); if (!receiver) { return res.status(404).json({ message: "Receiver user not found." }); } - + // Sanitize message text if present + const sanitizedMessage = message ? xss(message.trim()) : ""; + // Validate and upload image + let imageUrl = ""; + if (image) { + const validation = validateImageAttachment(image); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(image); + imageUrl = result.secure_url; + } + // Validate and upload audio + let audioUrl = ""; + if (audio) { + const validation = validateAudioAttachment(audio); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(audio, { resource_type: "auto" }); + audioUrl = result.secure_url; + } // Create scheduled message const scheduledMessage = new ScheduledMessage({ senderId, receiverId, - message: message || "", - image: image || "", - audio: audio || "", + message: sanitizedMessage, + image: imageUrl, + audio: audioUrl, scheduledFor: scheduledDate, replyTo: replyTo || null, status: "pending", }); - await scheduledMessage.save(); await scheduledMessage.populate("senderId receiverId", "name profilePicture"); - res.status(201).json({ message: "Message scheduled successfully.", data: scheduledMessage, diff --git a/backend/src/lib/attachmentValidator.js b/backend/src/lib/attachmentValidator.js new file mode 100644 index 0000000..4d2a6ec --- /dev/null +++ b/backend/src/lib/attachmentValidator.js @@ -0,0 +1,52 @@ +/* + * Attachment validation utilities shared between live message handling and scheduled messages. + * These functions validate Base64 data URLs for images and audio files, enforce MIME allow‑lists, + * and ensure size limits (5 MB for images, 10 MB for audio). They return an object + * { isValid: boolean, error?: string }. + */ + +export const validateImageAttachment = (base64Str, maxSizeBytes = 5 * 1024 * 1024) => { + // Verify Data URL format + const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); + if (!match) { + return { isValid: false, error: "Invalid file format structure or corrupt payload." }; + } + const mimeType = match[1]; + const rawData = match[2]; + const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; + if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { + return { isValid: false, error: "Unsupported image signature type. Allowed formats: JPEG, PNG, WEBP, GIF." }; + } + // Estimate binary size + const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); + if (binarySizeEstimate > maxSizeBytes) { + return { isValid: false, error: "File boundary limit exceeded. Image size must be under 5MB." }; + } + return { isValid: true }; +}; + +export const validateAudioAttachment = (base64Str, maxSizeBytes = 10 * 1024 * 1024) => { + const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); + if (!match) { + return { isValid: false, error: "Invalid audio format structure or corrupt payload." }; + } + const mimeType = match[1]; + const rawData = match[2]; + const ALLOWED_MIME_TYPES = [ + "audio/webm", + "audio/mp3", + "audio/wav", + "audio/mpeg", + "audio/ogg", + "audio/x-m4a", + "audio/m4a", + ]; + if (!ALLOWED_MIME_TYPES.includes(mimeType.toLowerCase())) { + return { isValid: false, error: "Unsupported audio format. Allowed formats: WEBM, MP3, WAV, OGG, M4A." }; + } + const binarySizeEstimate = Math.floor((rawData.length * 3) / 4) - (rawData.endsWith("==") ? 2 : rawData.endsWith("=") ? 1 : 0); + if (binarySizeEstimate > maxSizeBytes) { + return { isValid: false, error: "Audio size exceeds the 10MB limit." }; + } + return { isValid: true }; +}; diff --git a/backend/src/lib/messageScheduler.js b/backend/src/lib/messageScheduler.js index f60231c..a8fd62a 100644 --- a/backend/src/lib/messageScheduler.js +++ b/backend/src/lib/messageScheduler.js @@ -2,6 +2,9 @@ import ScheduledMessage from "../models/scheduledMessage.model.js"; import Message from "../models/message.model.js"; import { io, getReceiverSocketIds } from "./socket.js"; import webpush from "./webpush.js"; +import xss from "xss"; +import cloudinary from "./cloudinary.js"; +import { validateImageAttachment, validateAudioAttachment } from "./attachmentValidator.js"; let schedulerInterval = null; @@ -13,13 +16,48 @@ const SCHEDULER_INTERVAL = 30000; // Check every 30 seconds */ async function processScheduledMessage(scheduledMsg) { try { + // Sanitize message text + const sanitizedMessage = scheduledMsg.message ? xss(scheduledMsg.message.trim()) : ""; + + // Handle image attachment + let imageUrl = ""; + if (scheduledMsg.image) { + if (scheduledMsg.image.startsWith('data:')) { + const validation = validateImageAttachment(scheduledMsg.image); + if (!validation.isValid) { + console.error(`[Scheduler] Image validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + return; // Skip processing this message + } + const uploadResult = await cloudinary.uploader.upload(scheduledMsg.image); + imageUrl = uploadResult.secure_url; + } else { + imageUrl = scheduledMsg.image; // Assume already a URL + } + } + + // Handle audio attachment + let audioUrl = ""; + if (scheduledMsg.audio) { + if (scheduledMsg.audio.startsWith('data:')) { + const validation = validateAudioAttachment(scheduledMsg.audio); + if (!validation.isValid) { + console.error(`[Scheduler] Audio validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + return; // Skip processing this message + } + const uploadResult = await cloudinary.uploader.upload(scheduledMsg.audio, { resource_type: "auto" }); + audioUrl = uploadResult.secure_url; + } else { + audioUrl = scheduledMsg.audio; // Assume already a URL + } + } + // Create the actual message const newMessage = new Message({ senderId: scheduledMsg.senderId, receiverId: scheduledMsg.receiverId, - message: scheduledMsg.message, - image: scheduledMsg.image, - audio: scheduledMsg.audio, + message: sanitizedMessage, + image: imageUrl, + audio: audioUrl, replyTo: scheduledMsg.replyTo, status: "sent", }); From 6d48ae6e275455a089a0fcb0c9cce90af6a4fa25 Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Sat, 13 Jun 2026 12:02:12 +0530 Subject: [PATCH 4/4] fix(scheduled): resolve CodeRabbit review comments --- .../scheduledMessage.controller.js | 31 +++++++++++++++++-- backend/src/lib/attachmentValidator.js | 2 ++ backend/src/lib/messageScheduler.js | 8 ++++- backend/src/models/scheduledMessage.model.js | 3 +- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/backend/src/controllers/scheduledMessage.controller.js b/backend/src/controllers/scheduledMessage.controller.js index 406dd54..78bef5d 100644 --- a/backend/src/controllers/scheduledMessage.controller.js +++ b/backend/src/controllers/scheduledMessage.controller.js @@ -188,9 +188,34 @@ export async function updateScheduledMessage(req, res) { } // Update fields if provided - if (message !== undefined) scheduledMessage.message = message; - if (image !== undefined) scheduledMessage.image = image; - if (audio !== undefined) scheduledMessage.audio = audio; + if (message !== undefined) scheduledMessage.message = message ? xss(message.trim()) : ""; + + if (image !== undefined) { + if (image) { + const validation = validateImageAttachment(image); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(image); + scheduledMessage.image = result.secure_url; + } else { + scheduledMessage.image = ""; + } + } + + if (audio !== undefined) { + if (audio) { + const validation = validateAudioAttachment(audio); + if (!validation.isValid) { + return res.status(400).json({ message: validation.error }); + } + const result = await cloudinary.uploader.upload(audio, { resource_type: "auto" }); + scheduledMessage.audio = result.secure_url; + } else { + scheduledMessage.audio = ""; + } + } + if (replyTo !== undefined) scheduledMessage.replyTo = replyTo; if (scheduledFor) { diff --git a/backend/src/lib/attachmentValidator.js b/backend/src/lib/attachmentValidator.js index 4d2a6ec..31731ae 100644 --- a/backend/src/lib/attachmentValidator.js +++ b/backend/src/lib/attachmentValidator.js @@ -6,6 +6,7 @@ */ export const validateImageAttachment = (base64Str, maxSizeBytes = 5 * 1024 * 1024) => { + if (typeof base64Str !== 'string' || !base64Str) return { isValid: false, error: "Invalid file format structure or corrupt payload." }; // Verify Data URL format const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); if (!match) { @@ -26,6 +27,7 @@ export const validateImageAttachment = (base64Str, maxSizeBytes = 5 * 1024 * 102 }; export const validateAudioAttachment = (base64Str, maxSizeBytes = 10 * 1024 * 1024) => { + if (typeof base64Str !== 'string' || !base64Str) return { isValid: false, error: "Invalid audio format structure or corrupt payload." }; const match = base64Str.match(/^data:([^;]+);base64,(.+)$/); if (!match) { return { isValid: false, error: "Invalid audio format structure or corrupt payload." }; diff --git a/backend/src/lib/messageScheduler.js b/backend/src/lib/messageScheduler.js index a8fd62a..7bebd33 100644 --- a/backend/src/lib/messageScheduler.js +++ b/backend/src/lib/messageScheduler.js @@ -26,6 +26,9 @@ async function processScheduledMessage(scheduledMsg) { const validation = validateImageAttachment(scheduledMsg.image); if (!validation.isValid) { console.error(`[Scheduler] Image validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + scheduledMsg.status = "failed"; + scheduledMsg.failureReason = validation.error; + await scheduledMsg.save(); return; // Skip processing this message } const uploadResult = await cloudinary.uploader.upload(scheduledMsg.image); @@ -42,6 +45,9 @@ async function processScheduledMessage(scheduledMsg) { const validation = validateAudioAttachment(scheduledMsg.audio); if (!validation.isValid) { console.error(`[Scheduler] Audio validation failed for scheduled message ${scheduledMsg._id}:`, validation.error); + scheduledMsg.status = "failed"; + scheduledMsg.failureReason = validation.error; + await scheduledMsg.save(); return; // Skip processing this message } const uploadResult = await cloudinary.uploader.upload(scheduledMsg.audio, { resource_type: "auto" }); @@ -84,7 +90,7 @@ async function processScheduledMessage(scheduledMsg) { // For now, this is a placeholder for push notification logic const notificationPayload = { title: "New message", - body: scheduledMsg.message || "(Message with attachment)", + body: sanitizedMessage || "(Message with attachment)", icon: "/icon.png", }; diff --git a/backend/src/models/scheduledMessage.model.js b/backend/src/models/scheduledMessage.model.js index 4db072a..6c79e2b 100644 --- a/backend/src/models/scheduledMessage.model.js +++ b/backend/src/models/scheduledMessage.model.js @@ -8,7 +8,8 @@ const scheduledMessageSchema = new mongoose.Schema( image: { type: String, default: "" }, audio: { type: String, default: "" }, scheduledFor: { type: Date, required: true }, - status: { type: String, enum: ["pending", "sent", "cancelled"], default: "pending" }, + status: { type: String, enum: ["pending", "sent", "cancelled", "failed"], default: "pending" }, + failureReason: { type: String, default: null }, sentAt: { type: Date, default: null }, cancelledAt: { type: Date, default: null }, replyTo: {