From 0781e133e6c1560de711f9cdd8b0571229f1153d Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Thu, 27 Feb 2025 12:52:24 -0600 Subject: [PATCH] Fixed validation --- src/pages/api/rsvp.ts | 105 +++++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/src/pages/api/rsvp.ts b/src/pages/api/rsvp.ts index ae4c0f1..f0a0185 100644 --- a/src/pages/api/rsvp.ts +++ b/src/pages/api/rsvp.ts @@ -30,6 +30,46 @@ const objectsToCSV = (data: RSVPItem[]): string => { return csvRows.join("\n"); }; +// Sanitization helpers +const sanitizeString = (value: unknown): string => { + if (typeof value !== 'string') return ''; + // Remove any control characters and normalize whitespace + return value + .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Remove control characters + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); +}; + +const sanitizeBoolean = (value: unknown): boolean => { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') { + const lowered = value.toLowerCase().trim(); + return !(lowered === 'false' || lowered === '0' || lowered === 'no'); + } + return Boolean(value); +}; + +const sanitizeTimestamp = (value: unknown): string => { + if (typeof value !== 'string') return new Date().toISOString(); + try { + const date = new Date(value); + if (isNaN(date.getTime())) throw new Error('Invalid date'); + return date.toISOString(); + } catch { + return new Date().toISOString(); + } +}; + +const sanitizeRSVP = (data: Record): RSVPItem => { + return { + name: sanitizeString(data.name), + attending: sanitizeBoolean(data.attending), + dietaryRestrictions: sanitizeString(data.dietaryRestrictions), + notes: sanitizeString(data.notes), + timestamp: sanitizeTimestamp(data.timestamp), + }; +}; + const csvToObjects = (csv: string): RSVPItem[] => { const lines = csv.split("\n"); const headers = lines[0].split(",").map((h) => h.trim()); @@ -42,6 +82,7 @@ const csvToObjects = (csv: string): RSVPItem[] => { const values: string[] = []; let inQuote = false; let currentValue = ''; + let isEscaped = false; for (let i = 0; i < line.length; i++) { const char = line[i]; @@ -51,41 +92,37 @@ const csvToObjects = (csv: string): RSVPItem[] => { // Handle escaped quotes currentValue += '"'; i++; - } else { + isEscaped = true; + } else if (!isEscaped || (isEscaped && inQuote)) { inQuote = !inQuote; + isEscaped = false; + } else { + currentValue += char; } } else if (char === ',' && !inQuote) { values.push(currentValue.trim()); currentValue = ''; + isEscaped = false; } else { currentValue += char; } } values.push(currentValue.trim()); - // Create the entry object - const entry: Partial = {}; + // Create a data object from the CSV values + const data: Record = {}; headers.forEach((header, index) => { - let value = values[index] || ""; - value = value.replace(/^"(.*)"$/, "$1").replace(/""/g, '"'); - - if (header === "attending") { - // Explicitly convert to boolean - entry.attending = value.toLowerCase() === "true"; - } else { - entry[header as keyof RSVPItem] = value; + if (index < values.length) { + let value = values[index]; + value = value.replace(/^"(.*)"$/, "$1").replace(/""/g, '"'); + data[header] = value; } }); - return entry as RSVPItem; + // Sanitize the data + return sanitizeRSVP(data); }) - .filter(entry => - // Filter out malformed entries - entry.name && - typeof entry.attending === 'boolean' && - typeof entry.timestamp === 'string' && - entry.timestamp.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) - ); + .filter(entry => entry.name.length > 0); // Only keep entries with non-empty names }; // GET: Retrieve all RSVPs (requires admin role) @@ -133,7 +170,27 @@ const handlePost: APIRoute = async ({ request }) => { try { const FILE_KEY = "rsvp.csv"; - const newRsvp = await request.json(); + const rawData = await request.json(); + + // Validate required fields + if (!rawData.name) { + return new Response( + JSON.stringify({ + success: false, + error: "Name is required", + }), + { + status: 400, + headers, + } + ); + } + + // Sanitize the input data + const newRsvp = sanitizeRSVP({ + ...rawData, + timestamp: rawData.timestamp || new Date().toISOString(), + }); let existingRsvps: RSVPItem[] = []; const fileContent = await getS3Data(FILE_KEY); @@ -141,13 +198,7 @@ const handlePost: APIRoute = async ({ request }) => { existingRsvps = csvToObjects(fileContent); } - existingRsvps.push({ - ...newRsvp, - attending: Boolean(newRsvp.attending), - notes: newRsvp.notes || "", - dietaryRestrictions: newRsvp.dietaryRestrictions || "", - timestamp: newRsvp.timestamp || new Date().toISOString(), - }); + existingRsvps.push(newRsvp); const csvContent = objectsToCSV(existingRsvps); await putS3Data(FILE_KEY, csvContent, "text/csv");