From b1bb2d85cc7998d04e5066f83330a1e506a34dca Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Fri, 7 Aug 2026 23:13:17 +0530 Subject: [PATCH 1/4] Fix Memory Exhaustion and DoS for PDF uploads (#1549) --- backend/controllers/notesSummaryController.js | 9 +++++++- backend/controllers/resumeController.js | 11 +++++++++- backend/middlewares/uploadMiddleware.js | 21 ++++++++++++++----- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/backend/controllers/notesSummaryController.js b/backend/controllers/notesSummaryController.js index e8d6eac1..dc8e13d2 100644 --- a/backend/controllers/notesSummaryController.js +++ b/backend/controllers/notesSummaryController.js @@ -1,5 +1,6 @@ const axios = require("axios"); const crypto = require("crypto"); +const fs = require("fs"); const { generateWithFallback } = require("../utils/geminiHelper"); const { inspectPdfBuffer, @@ -103,6 +104,7 @@ function parseAiJson(raw) { * @example */ const summarizeNotes = async (req, res) => { + let uploadedFilePath = null; try { let buffer; let fileName; @@ -110,7 +112,8 @@ const summarizeNotes = async (req, res) => { let sourceUrl = null; if (req.file) { - buffer = req.file.buffer; + uploadedFilePath = req.file.path; + buffer = await fs.promises.readFile(uploadedFilePath); fileName = req.file.originalname; sourceType = "upload"; } else { @@ -238,6 +241,10 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec } res.status(500).json({ success: false, message: "Failed to summarize notes." }); + } finally { + if (uploadedFilePath) { + fs.promises.unlink(uploadedFilePath).catch(err => console.error("Failed to delete temp notes PDF:", err)); + } } }; diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index e4f8070c..8c2336d5 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -1,5 +1,6 @@ const axios = require('axios'); const FormData = require('form-data'); +const fs = require('fs'); const { generateWithFallback } = require('../utils/geminiHelper'); /** @@ -100,11 +101,15 @@ const compileResume = async (req, res) => { * } */ const analyzeResume = async (req, res) => { + let uploadedFilePath = null; try { if (!req.file) { return res.status(400).json({ message: "No resume file uploaded" }); } + uploadedFilePath = req.file.path; + const fileBuffer = await fs.promises.readFile(uploadedFilePath); + const targetRole = req.body.targetRole || "General Professional"; // 2. Prompt Engineering @@ -133,7 +138,7 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw prompt, { inlineData: { - data: req.file.buffer.toString("base64"), + data: fileBuffer.toString("base64"), mimeType: "application/pdf" } } @@ -161,6 +166,10 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw } catch (error) { console.error("Resume Analysis Error:", error); res.status(500).json({ message: "Failed to analyze resume" }); + } finally { + if (uploadedFilePath) { + fs.promises.unlink(uploadedFilePath).catch(err => console.error("Failed to delete temp resume PDF:", err)); + } } } diff --git a/backend/middlewares/uploadMiddleware.js b/backend/middlewares/uploadMiddleware.js index c64b7855..ee2afd83 100644 --- a/backend/middlewares/uploadMiddleware.js +++ b/backend/middlewares/uploadMiddleware.js @@ -1,7 +1,7 @@ const multer = require("multer"); const fs = require("fs"); const path = require("path"); -const { fileTypeFromBuffer } = require("file-type"); +const { fileTypeFromBuffer, fileTypeFromFile } = require("file-type"); const MAX_FILE_SIZE = 5 * 1024 * 1024; @@ -120,7 +120,12 @@ const validateResumeMagicBytes = async (req, res, next) => { } try { - const fileType = await fileTypeFromBuffer(req.file.buffer); + let fileType; + if (req.file.buffer) { + fileType = await fileTypeFromBuffer(req.file.buffer); + } else if (req.file.path) { + fileType = await fileTypeFromFile(req.file.path); + } const allowedMimeTypes = new Set([ 'application/pdf', @@ -153,9 +158,12 @@ const upload = multer({ limits: { fileSize: MAX_FILE_SIZE }, }); -// Upload instance for resumes (memory storage) +// Upload instance for resumes (disk storage) const uploadResume = multer({ - storage: multer.memoryStorage(), + storage: multer.diskStorage({ + destination: (req, file, cb) => cb(null, "uploads/"), + filename: (req, file, cb) => cb(null, `${Date.now()}-${sanitizeBase(file.originalname)}.pdf`) + }), fileFilter: resumeFileFilter, limits: { fileSize: MAX_FILE_SIZE }, }); @@ -164,7 +172,10 @@ const uploadResume = multer({ const NOTES_MAX_FILE_SIZE = 15 * 1024 * 1024; const uploadNotes = multer({ - storage: multer.memoryStorage(), + storage: multer.diskStorage({ + destination: (req, file, cb) => cb(null, "uploads/"), + filename: (req, file, cb) => cb(null, `${Date.now()}-${sanitizeBase(file.originalname)}.pdf`) + }), fileFilter: resumeFileFilter, // PDF-only, same filter as resumes limits: { fileSize: NOTES_MAX_FILE_SIZE }, }); From 9ca5b8af0e259ea050c17ad86a92242c4619a187 Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Fri, 7 Aug 2026 23:25:33 +0530 Subject: [PATCH 2/4] Address CodeRabbit AI and CodeQL review feedback --- .../Input_validators/ValidateNotesSummary.js | 2 ++ backend/Input_validators/ValidateResume.js | 2 ++ backend/controllers/notesSummaryController.js | 9 ++++++--- backend/controllers/resumeController.js | 9 ++++++--- backend/middlewares/uploadMiddleware.js | 18 ++++++++++-------- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/Input_validators/ValidateNotesSummary.js b/backend/Input_validators/ValidateNotesSummary.js index 970dff1b..4ad6c04a 100644 --- a/backend/Input_validators/ValidateNotesSummary.js +++ b/backend/Input_validators/ValidateNotesSummary.js @@ -28,6 +28,7 @@ const validateSummarizeNotes = (req, res, next) => { try { req.body = summarizeRequestSchema.parse(req.body || {}); if (!req.file && !req.body.url) { + if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return res.status(400).json({ success: false, message: "Please upload a PDF or choose one from Notes & Books.", @@ -35,6 +36,7 @@ const validateSummarizeNotes = (req, res, next) => { } next(); } catch (error) { + if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return handleValidationError(res, error); } }; diff --git a/backend/Input_validators/ValidateResume.js b/backend/Input_validators/ValidateResume.js index 7e5b0b20..fa29da39 100644 --- a/backend/Input_validators/ValidateResume.js +++ b/backend/Input_validators/ValidateResume.js @@ -50,10 +50,12 @@ const validateAnalyzeResume = (req, res, next) => { analyzeResumeSchema.parse(req.body); // also ensure file is uploaded if (!req.file) { + if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return res.status(400).json({ success: false, message: "No resume file uploaded" }); } next(); } catch (error) { + if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return handleValidationError(res, error); } }; diff --git a/backend/controllers/notesSummaryController.js b/backend/controllers/notesSummaryController.js index dc8e13d2..44120740 100644 --- a/backend/controllers/notesSummaryController.js +++ b/backend/controllers/notesSummaryController.js @@ -112,7 +112,7 @@ const summarizeNotes = async (req, res) => { let sourceUrl = null; if (req.file) { - uploadedFilePath = req.file.path; + uploadedFilePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); buffer = await fs.promises.readFile(uploadedFilePath); fileName = req.file.originalname; sourceType = "upload"; @@ -142,6 +142,9 @@ const summarizeNotes = async (req, res) => { const readingTime = computeReadingTime(pdfStats); const contentHash = crypto.createHash("sha256").update(buffer).digest("hex"); + const fileSize = buffer.length; + const base64Data = buffer.toString("base64"); + buffer = null; // Release Buffer memory const prompt = `You are an expert academic tutor helping a student decide whether a set of study notes is useful before they read it. @@ -171,7 +174,7 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec prompt, { inlineData: { - data: buffer.toString("base64"), + data: base64Data, mimeType: "application/pdf", }, }, @@ -205,7 +208,7 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec res.status(200).json({ success: true, fileName, - fileSize: buffer.length, + fileSize, sourceType, sourceUrl, pageCount: pdfStats.numPages, diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index 8c2336d5..b2d1a530 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -107,11 +107,14 @@ const analyzeResume = async (req, res) => { return res.status(400).json({ message: "No resume file uploaded" }); } - uploadedFilePath = req.file.path; - const fileBuffer = await fs.promises.readFile(uploadedFilePath); + uploadedFilePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); + let fileBuffer = await fs.promises.readFile(uploadedFilePath); const targetRole = req.body.targetRole || "General Professional"; + const base64Data = fileBuffer.toString("base64"); + fileBuffer = null; // Release Buffer memory + // 2. Prompt Engineering const prompt = `You are an expert ATS (Applicant Tracking System) and Senior Technical Recruiter. Analyze the attached PDF resume against the target role: "${targetRole}". @@ -138,7 +141,7 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw prompt, { inlineData: { - data: fileBuffer.toString("base64"), + data: base64Data, mimeType: "application/pdf" } } diff --git a/backend/middlewares/uploadMiddleware.js b/backend/middlewares/uploadMiddleware.js index ee2afd83..be93f106 100644 --- a/backend/middlewares/uploadMiddleware.js +++ b/backend/middlewares/uploadMiddleware.js @@ -1,6 +1,8 @@ const multer = require("multer"); const fs = require("fs"); const path = require("path"); +const os = require("os"); +const crypto = require("crypto"); const { fileTypeFromBuffer, fileTypeFromFile } = require("file-type"); const MAX_FILE_SIZE = 5 * 1024 * 1024; @@ -128,20 +130,20 @@ const validateResumeMagicBytes = async (req, res, next) => { } const allowedMimeTypes = new Set([ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + 'application/pdf' ]); if (!fileType || !allowedMimeTypes.has(fileType.mime)) { + if (req.file.path) fs.promises.unlink(req.file.path).catch(() => {}); return res.status(400).json({ success: false, - message: 'Invalid file type. Only PDF and Word documents (.pdf, .doc, .docx) are allowed.' + message: 'Invalid file type. Only PDF documents are allowed.' }); } next(); } catch (error) { + if (req.file && req.file.path) fs.promises.unlink(req.file.path).catch(() => {}); console.error('Error validating file type:', error); return res.status(500).json({ success: false, @@ -161,8 +163,8 @@ const upload = multer({ // Upload instance for resumes (disk storage) const uploadResume = multer({ storage: multer.diskStorage({ - destination: (req, file, cb) => cb(null, "uploads/"), - filename: (req, file, cb) => cb(null, `${Date.now()}-${sanitizeBase(file.originalname)}.pdf`) + destination: (req, file, cb) => cb(null, os.tmpdir()), + filename: (req, file, cb) => cb(null, crypto.randomBytes(16).toString("hex") + ".pdf") }), fileFilter: resumeFileFilter, limits: { fileSize: MAX_FILE_SIZE }, @@ -173,8 +175,8 @@ const NOTES_MAX_FILE_SIZE = 15 * 1024 * 1024; const uploadNotes = multer({ storage: multer.diskStorage({ - destination: (req, file, cb) => cb(null, "uploads/"), - filename: (req, file, cb) => cb(null, `${Date.now()}-${sanitizeBase(file.originalname)}.pdf`) + destination: (req, file, cb) => cb(null, os.tmpdir()), + filename: (req, file, cb) => cb(null, crypto.randomBytes(16).toString("hex") + ".pdf") }), fileFilter: resumeFileFilter, // PDF-only, same filter as resumes limits: { fileSize: NOTES_MAX_FILE_SIZE }, From dc3ea2da5b3ab371bd5fa91bb9106755f4cd3b2b Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Fri, 7 Aug 2026 23:49:05 +0530 Subject: [PATCH 3/4] Further CodeRabbit and CodeQL feedback: Await unlinks, log errors, clean validation paths, dynamic file-type import --- .../Input_validators/ValidateNotesSummary.js | 8 ++++++-- backend/Input_validators/ValidateResume.js | 8 ++++++-- backend/controllers/notesSummaryController.js | 4 +++- backend/controllers/resumeController.js | 4 +++- backend/middlewares/uploadMiddleware.js | 17 ++++++++++++++--- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/backend/Input_validators/ValidateNotesSummary.js b/backend/Input_validators/ValidateNotesSummary.js index 4ad6c04a..0faf5267 100644 --- a/backend/Input_validators/ValidateNotesSummary.js +++ b/backend/Input_validators/ValidateNotesSummary.js @@ -28,7 +28,6 @@ const validateSummarizeNotes = (req, res, next) => { try { req.body = summarizeRequestSchema.parse(req.body || {}); if (!req.file && !req.body.url) { - if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return res.status(400).json({ success: false, message: "Please upload a PDF or choose one from Notes & Books.", @@ -36,7 +35,12 @@ const validateSummarizeNotes = (req, res, next) => { } next(); } catch (error) { - if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); + if (req.file && req.file.path) { + const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); + require('fs').promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } return handleValidationError(res, error); } }; diff --git a/backend/Input_validators/ValidateResume.js b/backend/Input_validators/ValidateResume.js index fa29da39..20511547 100644 --- a/backend/Input_validators/ValidateResume.js +++ b/backend/Input_validators/ValidateResume.js @@ -50,12 +50,16 @@ const validateAnalyzeResume = (req, res, next) => { analyzeResumeSchema.parse(req.body); // also ensure file is uploaded if (!req.file) { - if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); return res.status(400).json({ success: false, message: "No resume file uploaded" }); } next(); } catch (error) { - if (req.file && req.file.path) require('fs').promises.unlink(req.file.path).catch(() => {}); + if (req.file && req.file.path) { + const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); + require('fs').promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } return handleValidationError(res, error); } }; diff --git a/backend/controllers/notesSummaryController.js b/backend/controllers/notesSummaryController.js index 44120740..05335bb7 100644 --- a/backend/controllers/notesSummaryController.js +++ b/backend/controllers/notesSummaryController.js @@ -246,7 +246,9 @@ DO NOT wrap the response in markdown code blocks. Return ONLY the raw JSON objec res.status(500).json({ success: false, message: "Failed to summarize notes." }); } finally { if (uploadedFilePath) { - fs.promises.unlink(uploadedFilePath).catch(err => console.error("Failed to delete temp notes PDF:", err)); + await require('fs').promises.unlink(uploadedFilePath).catch(err => { + if (err.code !== 'ENOENT') console.error("Failed to delete temp notes PDF:", err); + }); } } }; diff --git a/backend/controllers/resumeController.js b/backend/controllers/resumeController.js index b2d1a530..d21b54d7 100644 --- a/backend/controllers/resumeController.js +++ b/backend/controllers/resumeController.js @@ -171,7 +171,9 @@ DO NOT wrap the response in markdown blocks like \`\`\`json. Return ONLY the raw res.status(500).json({ message: "Failed to analyze resume" }); } finally { if (uploadedFilePath) { - fs.promises.unlink(uploadedFilePath).catch(err => console.error("Failed to delete temp resume PDF:", err)); + await require('fs').promises.unlink(uploadedFilePath).catch(err => { + if (err.code !== 'ENOENT') console.error("Failed to delete temp resume PDF:", err); + }); } } } diff --git a/backend/middlewares/uploadMiddleware.js b/backend/middlewares/uploadMiddleware.js index be93f106..7a28ad71 100644 --- a/backend/middlewares/uploadMiddleware.js +++ b/backend/middlewares/uploadMiddleware.js @@ -3,7 +3,6 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); const crypto = require("crypto"); -const { fileTypeFromBuffer, fileTypeFromFile } = require("file-type"); const MAX_FILE_SIZE = 5 * 1024 * 1024; @@ -52,6 +51,7 @@ const IMAGE_EXTENSION_BY_MIME = { * @returns {Promise} Stored filename, or null if content is not an image. */ const resolveImageFileName = async (file) => { + const { fileTypeFromBuffer } = await import("file-type"); const fileType = await fileTypeFromBuffer(file.buffer); const ext = fileType && IMAGE_EXTENSION_BY_MIME[fileType.mime]; if (!ext) return null; @@ -122,6 +122,7 @@ const validateResumeMagicBytes = async (req, res, next) => { } try { + const { fileTypeFromBuffer, fileTypeFromFile } = await import("file-type"); let fileType; if (req.file.buffer) { fileType = await fileTypeFromBuffer(req.file.buffer); @@ -134,7 +135,12 @@ const validateResumeMagicBytes = async (req, res, next) => { ]); if (!fileType || !allowedMimeTypes.has(fileType.mime)) { - if (req.file.path) fs.promises.unlink(req.file.path).catch(() => {}); + if (req.file.path) { + const safePath = path.join(os.tmpdir(), path.basename(req.file.path)); + fs.promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } return res.status(400).json({ success: false, message: 'Invalid file type. Only PDF documents are allowed.' @@ -143,7 +149,12 @@ const validateResumeMagicBytes = async (req, res, next) => { next(); } catch (error) { - if (req.file && req.file.path) fs.promises.unlink(req.file.path).catch(() => {}); + if (req.file && req.file.path) { + const safePath = path.join(os.tmpdir(), path.basename(req.file.path)); + fs.promises.unlink(safePath).catch((err) => { + if (err.code !== 'ENOENT') console.error('Cleanup error:', err); + }); + } console.error('Error validating file type:', error); return res.status(500).json({ success: false, From 2759ba25036b8852b770c6016728ed3c4ff08ed8 Mon Sep 17 00:00:00 2001 From: Dev1822 Date: Fri, 7 Aug 2026 23:54:39 +0530 Subject: [PATCH 4/4] Make validators async and await unlink cleanup --- backend/Input_validators/ValidateNotesSummary.js | 4 ++-- backend/Input_validators/ValidateResume.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/Input_validators/ValidateNotesSummary.js b/backend/Input_validators/ValidateNotesSummary.js index 0faf5267..fc754d25 100644 --- a/backend/Input_validators/ValidateNotesSummary.js +++ b/backend/Input_validators/ValidateNotesSummary.js @@ -24,7 +24,7 @@ const summarizeRequestSchema = z.object({ fileName: z.string().max(200).optional(), }); -const validateSummarizeNotes = (req, res, next) => { +const validateSummarizeNotes = async (req, res, next) => { try { req.body = summarizeRequestSchema.parse(req.body || {}); if (!req.file && !req.body.url) { @@ -37,7 +37,7 @@ const validateSummarizeNotes = (req, res, next) => { } catch (error) { if (req.file && req.file.path) { const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); - require('fs').promises.unlink(safePath).catch((err) => { + await require('fs').promises.unlink(safePath).catch((err) => { if (err.code !== 'ENOENT') console.error('Cleanup error:', err); }); } diff --git a/backend/Input_validators/ValidateResume.js b/backend/Input_validators/ValidateResume.js index 20511547..0f45b8a5 100644 --- a/backend/Input_validators/ValidateResume.js +++ b/backend/Input_validators/ValidateResume.js @@ -45,7 +45,7 @@ const validateCompileResume = (req, res, next) => { }; // Middleware for analyzeResume -const validateAnalyzeResume = (req, res, next) => { +const validateAnalyzeResume = async (req, res, next) => { try { analyzeResumeSchema.parse(req.body); // also ensure file is uploaded @@ -56,7 +56,7 @@ const validateAnalyzeResume = (req, res, next) => { } catch (error) { if (req.file && req.file.path) { const safePath = require('path').join(require('os').tmpdir(), require('path').basename(req.file.path)); - require('fs').promises.unlink(safePath).catch((err) => { + await require('fs').promises.unlink(safePath).catch((err) => { if (err.code !== 'ENOENT') console.error('Cleanup error:', err); }); }