From 6be6d17da014f0ae123982abfecfb9ddf3446793 Mon Sep 17 00:00:00 2001 From: binarybandit Date: Mon, 1 Jun 2026 06:21:09 +0530 Subject: [PATCH] security: prevent CSV injection and fix ICS formatting --- backend/controllers/csvDownload.controller.js | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/backend/controllers/csvDownload.controller.js b/backend/controllers/csvDownload.controller.js index 45dc6f1..645a269 100644 --- a/backend/controllers/csvDownload.controller.js +++ b/backend/controllers/csvDownload.controller.js @@ -23,8 +23,18 @@ function escapeIcsText(value = '') { .replace(/;/g, '\\;'); } +// Complies with RFC 5545 (Max 75 characters per line) +// Folds lines at 75 characters, appending CRLF + Space for continuation +function foldIcsLine(line) { + if (line.length <= 75) return line; + return line.match(/.{1,74}/g).join('\r\n '); +} + function formatIcsDate(dateInput) { const date = new Date(dateInput); + + // Guard against Invalid Dates + if (isNaN(date.getTime())) return null; const year = date.getUTCFullYear(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); @@ -38,17 +48,19 @@ function formatIcsDate(dateInput) { function buildCalendarIcs(tasks = []) { const dtstamp = formatIcsDate(new Date().toISOString()); + const events = tasks - .filter(task => task.due_at) + .filter(task => task.due_at && !isNaN(new Date(task.due_at).getTime())) // Strict date validation .map(task => { const start = new Date(task.due_at); - const end = new Date(start.getTime() + 60 * 60 * 1000); + const end = new Date(start.getTime() + 60 * 60 * 1000); + const description = [ `Subject: ${task.subject_name || 'General'}`, `Status: ${task.status || 'Not Started'}`, `Priority: ${task.priority || 'medium'}`, `Notes: ${task.notes || 'None'}`, - ].join('\n'); + ].join('\\n'); return [ 'BEGIN:VEVENT', @@ -59,7 +71,7 @@ function buildCalendarIcs(tasks = []) { `SUMMARY:${escapeIcsText(task.title || 'Untitled task')}`, `DESCRIPTION:${escapeIcsText(description)}`, 'END:VEVENT', - ].join('\r\n'); + ].map(foldIcsLine).join('\r\n'); // Apply line folding to every event attribute }); return [ @@ -74,6 +86,26 @@ function buildCalendarIcs(tasks = []) { ].join('\r\n'); } +// Prevents CSV Injection and formatting corruption +function escapeCsvValue(value) { + if (value === null || value === undefined) return ''; + let str = String(value); + + // 1. MITIGATE CSV INJECTION (Formula Injection) + // If string starts with a dangerous character (=, +, -, @, \t, \r), prepend a single quote + if (/^[=+\-@\t\r]/.test(str)) { + str = "'" + str; + } + + // 2. PREVENT FORMATTING CORRUPTION + // If string contains a quote, comma, or newline, it MUST be wrapped in quotes + if (/[",\n\r]/.test(str)) { + return `"${str.replace(/"/g, '""')}"`; // Escape inner double-quotes + } + + return str; +} + async function downloadData(req, res) { try { const data = await getAllTasksWithSubjects(); @@ -88,11 +120,14 @@ async function downloadData(req, res) { task.status, task.priority, task.confidence_score, - `"${(task.notes || '').replace(/"/g, '""')}"`, + task.notes, ]), ]; - const csvString = rows.map(row => row.join(',')).join('\n'); + // Apply the secure escape function to every single cell before joining + const csvString = rows + .map(row => row.map(escapeCsvValue).join(',')) + .join('\n'); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', 'attachment; filename="study_data.csv"');