this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

added some basic file upload security

+266
+13
storygraph-to-goodreads/backend/index.ts
··· 9 9 generateCSV, 10 10 parseCSV, 11 11 } from "./utils/converter.ts"; 12 + import { validateCSVFile } from "./utils/file-validator.ts"; 12 13 import type { ConversionDirection } from "../shared/types.ts"; 13 14 14 15 const app = new Hono(); ··· 35 36 36 37 if (!file) { 37 38 return c.json({ error: "No file provided" }, 400); 39 + } 40 + 41 + // Validate file security 42 + const validation = await validateCSVFile(file); 43 + if (!validation.isValid) { 44 + return c.json({ error: validation.error }, 400); 38 45 } 39 46 40 47 if ( ··· 184 191 185 192 if (!file) { 186 193 return c.json({ error: "No file provided" }, 400); 194 + } 195 + 196 + // Validate file security 197 + const validation = await validateCSVFile(file); 198 + if (!validation.isValid) { 199 + return c.json({ error: validation.error }, 400); 187 200 } 188 201 189 202 if (
+108
storygraph-to-goodreads/backend/utils/file-validator.test.ts
··· 1 + import { assertEquals } from "https://deno.land/std@0.208.0/assert/mod.ts"; 2 + import { validateCSVFile } from "./file-validator.ts"; 3 + 4 + // Helper function to create a mock File object 5 + function createMockFile( 6 + content: string, 7 + filename: string, 8 + size?: number, 9 + ): File { 10 + const blob = new Blob([content], { type: "text/csv" }); 11 + const file = new File([blob], filename, { type: "text/csv" }); 12 + 13 + // Override size if provided (for testing size limits) 14 + if (size !== undefined) { 15 + Object.defineProperty(file, "size", { value: size, writable: false }); 16 + } 17 + 18 + return file; 19 + } 20 + 21 + Deno.test("validateCSVFile - should accept valid CSV file", async () => { 22 + const validCSV = "Title,Author,Rating\nBook 1,Author 1,5\nBook 2,Author 2,4"; 23 + const file = createMockFile(validCSV, "books.csv"); 24 + 25 + const result = await validateCSVFile(file); 26 + 27 + assertEquals(result.isValid, true); 28 + assertEquals(result.error, undefined); 29 + }); 30 + 31 + Deno.test("validateCSVFile - should reject non-CSV file extension", async () => { 32 + const content = "Title,Author,Rating\nBook 1,Author 1,5"; 33 + const file = createMockFile(content, "books.txt"); 34 + 35 + const result = await validateCSVFile(file); 36 + 37 + assertEquals(result.isValid, false); 38 + assertEquals(result.error, "Only CSV files are allowed"); 39 + }); 40 + 41 + Deno.test("validateCSVFile - should reject file exceeding 1MB", async () => { 42 + const content = "Title,Author,Rating\nBook 1,Author 1,5"; 43 + const file = createMockFile(content, "books.csv", 1024 * 1024 + 1); // 1MB + 1 byte 44 + 45 + const result = await validateCSVFile(file); 46 + 47 + assertEquals(result.isValid, false); 48 + assertEquals(result.error?.includes("File size exceeds 1MB limit"), true); 49 + }); 50 + 51 + Deno.test("validateCSVFile - should reject empty file", async () => { 52 + const file = createMockFile("", "books.csv"); 53 + 54 + const result = await validateCSVFile(file); 55 + 56 + assertEquals(result.isValid, false); 57 + assertEquals(result.error, "File is empty"); 58 + }); 59 + 60 + Deno.test("validateCSVFile - should reject file with only whitespace", async () => { 61 + const file = createMockFile(" \n\n \t ", "books.csv"); 62 + 63 + const result = await validateCSVFile(file); 64 + 65 + assertEquals(result.isValid, false); 66 + assertEquals(result.error, "File is empty"); 67 + }); 68 + 69 + Deno.test("validateCSVFile - should reject file with malicious script content", async () => { 70 + const maliciousCSV = "Title,Author\n<script>alert('xss')</script>,Author 1"; 71 + const file = createMockFile(maliciousCSV, "books.csv"); 72 + 73 + const result = await validateCSVFile(file); 74 + 75 + assertEquals(result.isValid, false); 76 + assertEquals(result.error, "File contains potentially malicious content"); 77 + }); 78 + 79 + Deno.test("validateCSVFile - should reject file with javascript: protocol", async () => { 80 + const maliciousCSV = "Title,Author\njavascript:alert('xss'),Author 1"; 81 + const file = createMockFile(maliciousCSV, "books.csv"); 82 + 83 + const result = await validateCSVFile(file); 84 + 85 + assertEquals(result.isValid, false); 86 + assertEquals(result.error, "File contains potentially malicious content"); 87 + }); 88 + 89 + Deno.test("validateCSVFile - should accept CSV with special characters", async () => { 90 + const csvWithSpecialChars = 91 + 'Title,Author,Notes\n"Book, with comma","Author\'s Name","Review: It\'s great!"'; 92 + const file = createMockFile(csvWithSpecialChars, "books.csv"); 93 + 94 + const result = await validateCSVFile(file); 95 + 96 + assertEquals(result.isValid, true); 97 + assertEquals(result.error, undefined); 98 + }); 99 + 100 + Deno.test("validateCSVFile - should accept single column CSV", async () => { 101 + const singleColumnCSV = "Title\nBook 1\nBook 2\nBook 3"; 102 + const file = createMockFile(singleColumnCSV, "books.csv"); 103 + 104 + const result = await validateCSVFile(file); 105 + 106 + assertEquals(result.isValid, true); 107 + assertEquals(result.error, undefined); 108 + });
+119
storygraph-to-goodreads/backend/utils/file-validator.ts
··· 1 + export interface FileValidationResult { 2 + isValid: boolean; 3 + error?: string; 4 + } 5 + 6 + export async function validateCSVFile( 7 + file: File, 8 + ): Promise<FileValidationResult> { 9 + // Check file size (max 1MB) 10 + const maxSize = 1024 * 1024; // 1MB in bytes 11 + if (file.size > maxSize) { 12 + return { 13 + isValid: false, 14 + error: `File size exceeds 1MB limit. Current size: ${ 15 + Math.round(file.size / 1024) 16 + }KB`, 17 + }; 18 + } 19 + 20 + // Check file extension 21 + if (!file.name.toLowerCase().endsWith(".csv")) { 22 + return { 23 + isValid: false, 24 + error: "Only CSV files are allowed", 25 + }; 26 + } 27 + 28 + try { 29 + // Get file content for validation 30 + const textContent = await file.text(); 31 + 32 + // Check if file is empty 33 + if (textContent.trim().length === 0) { 34 + return { 35 + isValid: false, 36 + error: "File is empty", 37 + }; 38 + } 39 + 40 + // Basic CSV format check - should have at least one line with content 41 + const lines = textContent.split("\n").filter((line) => 42 + line.trim().length > 0 43 + ); 44 + if (lines.length === 0) { 45 + return { 46 + isValid: false, 47 + error: "File contains no valid data", 48 + }; 49 + } 50 + 51 + // Check for potential malicious content patterns 52 + const suspiciousPatterns = [ 53 + /<script/i, 54 + /javascript:/i, 55 + /vbscript:/i, 56 + /onload=/i, 57 + /onerror=/i, 58 + /<iframe/i, 59 + /<object/i, 60 + /<embed/i, 61 + /data:text\/html/i, 62 + /data:application\/javascript/i, 63 + /<svg[^>]*onload/i, 64 + /<img[^>]*onerror/i, 65 + ]; 66 + 67 + for (const pattern of suspiciousPatterns) { 68 + if (pattern.test(textContent)) { 69 + return { 70 + isValid: false, 71 + error: "File contains potentially malicious content", 72 + }; 73 + } 74 + } 75 + 76 + // Check for suspicious file content that might indicate it's not actually a CSV 77 + // Look for binary file signatures at the beginning 78 + const firstBytes = textContent.substring(0, 10); 79 + const binarySignatures = [ 80 + "\x89PNG", // PNG 81 + "\xFF\xD8\xFF", // JPEG 82 + "GIF8", // GIF 83 + "%PDF", // PDF 84 + "PK\x03\x04", // ZIP 85 + "\x50\x4B", // ZIP variant 86 + ]; 87 + 88 + for (const signature of binarySignatures) { 89 + if (firstBytes.includes(signature)) { 90 + return { 91 + isValid: false, 92 + error: "File appears to be a binary file, not a CSV", 93 + }; 94 + } 95 + } 96 + 97 + // Additional validation: check if content looks like CSV 98 + // At least the first line should have some structure 99 + if (lines.length > 0) { 100 + const firstLine = lines[0]; 101 + // Very basic check - should contain some alphanumeric characters 102 + if (!/[a-zA-Z0-9]/.test(firstLine)) { 103 + return { 104 + isValid: false, 105 + error: "File does not appear to contain valid CSV data", 106 + }; 107 + } 108 + } 109 + 110 + return { isValid: true }; 111 + } catch (error) { 112 + return { 113 + isValid: false, 114 + error: `File validation error: ${ 115 + error instanceof Error ? error.message : "Unknown error" 116 + }`, 117 + }; 118 + } 119 + }
+26
storygraph-to-goodreads/frontend/components/App.tsx
··· 39 39 const fileInputRef = useRef<HTMLInputElement>(null); 40 40 41 41 const handleFileSelect = (file: File) => { 42 + // Client-side validation 42 43 if (!file.name.toLowerCase().endsWith(".csv")) { 43 44 setConversionState({ 44 45 stage: "error", 45 46 progress: 0, 46 47 message: "", 47 48 error: "Please select a CSV file", 49 + }); 50 + return; 51 + } 52 + 53 + // Check file size (1MB limit) 54 + const maxSize = 1024 * 1024; // 1MB 55 + if (file.size > maxSize) { 56 + setConversionState({ 57 + stage: "error", 58 + progress: 0, 59 + message: "", 60 + error: `File size exceeds 1MB limit. Current size: ${ 61 + Math.round(file.size / 1024) 62 + }KB`, 63 + }); 64 + return; 65 + } 66 + 67 + // Check if file is empty 68 + if (file.size === 0) { 69 + setConversionState({ 70 + stage: "error", 71 + progress: 0, 72 + message: "", 73 + error: "File is empty", 48 74 }); 49 75 return; 50 76 }