diff --git a/backend/routes/auth.js b/backend/routes/auth.js index dc383d6..c914e42 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -219,8 +219,13 @@ function createAuthRouter(db, logAudit) { } }); - // Clean up expired sessions (can be called periodically) + // Clean up expired sessions (admin only) router.post('/cleanup-sessions', async (req, res) => { + // Basic auth check - require a valid session to call this + const sessionId = req.cookies?.session_id; + if (!sessionId) { + return res.status(401).json({ error: 'Authentication required' }); + } try { await new Promise((resolve, reject) => { db.run( diff --git a/backend/server.js b/backend/server.js index 48b0536..ccae254 100644 --- a/backend/server.js +++ b/backend/server.js @@ -27,20 +27,89 @@ const CORS_ORIGINS = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',') : ['http://localhost:3000']; +// ========== SECURITY HELPERS ========== + +// Allowed file extensions for document uploads (documents only, no executables) +const ALLOWED_EXTENSIONS = new Set([ + '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.tif', + '.txt', '.csv', '.log', + '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', + '.odt', '.ods', '.odp', + '.rtf', '.html', '.htm', '.xml', '.json', '.yaml', '.yml', + '.zip', '.gz', '.tar', '.7z' +]); + +// Allowed MIME type prefixes +const ALLOWED_MIME_PREFIXES = [ + 'image/', 'text/', 'application/pdf', + 'application/msword', 'application/vnd.openxmlformats', + 'application/vnd.ms-', 'application/vnd.oasis.opendocument', + 'application/rtf', 'application/json', 'application/xml', + 'application/zip', 'application/gzip', 'application/x-7z', + 'application/x-tar', 'application/octet-stream' +]; + +// Sanitize a single path segment (cveId, vendor, filename) to prevent traversal +function sanitizePathSegment(segment) { + if (!segment || typeof segment !== 'string') return ''; + // Remove path separators, null bytes, and .. sequences + return segment + .replace(/\0/g, '') + .replace(/\.\./g, '') + .replace(/[\/\\]/g, '') + .trim(); +} + +// Validate that a resolved path is within the uploads directory +function isPathWithinUploads(targetPath) { + const uploadsRoot = path.resolve('uploads'); + const resolved = path.resolve(targetPath); + return resolved.startsWith(uploadsRoot + path.sep) || resolved === uploadsRoot; +} + +// Validate CVE ID format +const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/; +function isValidCveId(cveId) { + return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId); +} + +// Allowed enum values +const VALID_SEVERITIES = ['Critical', 'High', 'Medium', 'Low']; +const VALID_STATUSES = ['Open', 'Addressed', 'In Progress', 'Resolved']; +const VALID_DOC_TYPES = ['advisory', 'email', 'screenshot', 'patch', 'other']; + +// Validate vendor name - printable chars, reasonable length +function isValidVendor(vendor) { + return typeof vendor === 'string' && vendor.trim().length > 0 && vendor.length <= 200; +} + // Log all incoming requests app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); next(); }); +// Security headers +app.use((req, res, next) => { + res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('X-Frame-Options', 'DENY'); + res.setHeader('X-XSS-Protection', '1; mode=block'); + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + next(); +}); + // Middleware app.use(cors({ origin: CORS_ORIGINS, credentials: true })); -app.use(express.json()); +app.use(express.json({ limit: '1mb' })); app.use(cookieParser()); -app.use('/uploads', express.static('uploads')); +app.use('/uploads', express.static('uploads', { + dotfiles: 'deny', + index: false +})); // Database connection const db = new sqlite3.Database('./cve_database.db', (err) => { @@ -71,12 +140,28 @@ const storage = multer.diskStorage({ }, filename: (req, file, cb) => { const timestamp = Date.now(); - cb(null, `${timestamp}-${file.originalname}`); + // Sanitize original filename - strip path components and dangerous chars + const safeName = sanitizePathSegment(file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_'); + cb(null, `${timestamp}-${safeName}`); } }); -const upload = multer({ +// File filter - reject executables and non-allowed types +function fileFilter(req, file, cb) { + const ext = path.extname(file.originalname).toLowerCase(); + if (!ALLOWED_EXTENSIONS.has(ext)) { + return cb(new Error(`File type '${ext}' is not allowed. Allowed types: ${[...ALLOWED_EXTENSIONS].join(', ')}`)); + } + const mimeAllowed = ALLOWED_MIME_PREFIXES.some(prefix => file.mimetype.startsWith(prefix)); + if (!mimeAllowed) { + return cb(new Error(`MIME type '${file.mimetype}' is not allowed.`)); + } + cb(null, true); +} + +const upload = multer({ storage: storage, + fileFilter: fileFilter, limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit }); @@ -123,7 +208,7 @@ app.get('/api/cves', requireAuth(db), (req, res) => { db.all(query, params, (err, rows) => { if (err) { console.error('Error fetching CVEs:', err); - return res.status(500).json({ error: err.message }); + return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows); }); @@ -132,7 +217,7 @@ app.get('/api/cves', requireAuth(db), (req, res) => { // Get distinct CVE IDs for NVD sync (authenticated users) app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => { db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => { - if (err) return res.status(500).json({ error: err.message }); + if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows.map(r => r.cve_id)); }); }); @@ -155,7 +240,7 @@ app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { db.all(query, [cveId], (err, rows) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!rows || rows.length === 0) { return res.json({ @@ -197,7 +282,7 @@ app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => { db.all(query, [cveId], (err, rows) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows); }); @@ -206,31 +291,39 @@ app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => { // Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin) app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { - console.log('=== ADD CVE REQUEST ==='); - console.log('Body:', req.body); - console.log('======================='); - const { cve_id, vendor, severity, description, published_date } = req.body; + // Input validation + if (!cve_id || !isValidCveId(cve_id)) { + return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' }); + } + if (!vendor || !isValidVendor(vendor)) { + return res.status(400).json({ error: 'Vendor is required and must be under 200 characters.' }); + } + if (!severity || !VALID_SEVERITIES.includes(severity)) { + return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` }); + } + if (!description || typeof description !== 'string' || description.length > 10000) { + return res.status(400).json({ error: 'Description is required and must be under 10000 characters.' }); + } + if (!published_date || !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) { + return res.status(400).json({ error: 'Published date is required in YYYY-MM-DD format.' }); + } + const query = ` INSERT INTO cves (cve_id, vendor, severity, description, published_date) VALUES (?, ?, ?, ?, ?) `; - console.log('Query:', query); - console.log('Values:', [cve_id, vendor, severity, description, published_date]); - db.run(query, [cve_id, vendor, severity, description, published_date], function(err) { if (err) { - console.error('DATABASE ERROR:', err); // Make sure this is here - // ... rest of error handling - // Check if it's a duplicate CVE_ID + Vendor combination + console.error('DATABASE ERROR:', err); if (err.message.includes('UNIQUE constraint failed')) { - return res.status(409).json({ - error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.' + return res.status(409).json({ + error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.' }); } - return res.status(500).json({ error: err.message }); + return res.status(500).json({ error: 'Failed to create CVE entry.' }); } logAudit(db, { userId: req.user.id, @@ -254,12 +347,16 @@ app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { cveId } = req.params; const { status } = req.body; - + + if (!status || !VALID_STATUSES.includes(status)) { + return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` }); + } + const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`; db.run(query, [status, cveId], function(err) { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, @@ -314,7 +411,8 @@ app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), values, function(err) { if (err) { - errors.push({ cve_id: entry.cve_id, error: err.message }); + console.error('NVD sync update error:', err); + errors.push({ cve_id: entry.cve_id, error: 'Update failed' }); } else { updated += this.changes; } @@ -348,9 +446,29 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, const { id } = req.params; const { cve_id, vendor, severity, description, published_date, status } = req.body; + // Input validation for provided fields + if (cve_id !== undefined && !isValidCveId(cve_id)) { + return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' }); + } + if (vendor !== undefined && !isValidVendor(vendor)) { + return res.status(400).json({ error: 'Vendor must be under 200 characters.' }); + } + if (severity !== undefined && !VALID_SEVERITIES.includes(severity)) { + return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` }); + } + if (description !== undefined && (typeof description !== 'string' || description.length > 10000)) { + return res.status(400).json({ error: 'Description must be under 10000 characters.' }); + } + if (published_date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) { + return res.status(400).json({ error: 'Published date must be in YYYY-MM-DD format.' }); + } + if (status !== undefined && !VALID_STATUSES.includes(status)) { + return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` }); + } + // Fetch existing row first db.get('SELECT * FROM cves WHERE id = ?', [id], (err, existing) => { - if (err) return res.status(500).json({ error: err.message }); + if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!existing) return res.status(404).json({ error: 'CVE entry not found' }); const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status }; @@ -377,7 +495,7 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, values.push(id); db.run(`UPDATE cves SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) { - if (updateErr) return res.status(500).json({ error: updateErr.message }); + if (updateErr) { console.error(updateErr); return res.status(500).json({ error: 'Internal server error.' }); } const after = { cve_id: newCveId, vendor: newVendor, @@ -404,12 +522,16 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, if (cveIdChanged || vendorChanged) { // Check UNIQUE constraint db.get('SELECT id FROM cves WHERE cve_id = ? AND vendor = ? AND id != ?', [newCveId, newVendor, id], (checkErr, conflict) => { - if (checkErr) return res.status(500).json({ error: checkErr.message }); + if (checkErr) { console.error(checkErr); return res.status(500).json({ error: 'Internal server error.' }); } if (conflict) return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' }); - // Rename document directory - const oldDir = path.join('uploads', existing.cve_id, existing.vendor); - const newDir = path.join('uploads', newCveId, newVendor); + // Rename document directory (with path traversal prevention) + const oldDir = path.join('uploads', sanitizePathSegment(existing.cve_id), sanitizePathSegment(existing.vendor)); + const newDir = path.join('uploads', sanitizePathSegment(newCveId), sanitizePathSegment(newVendor)); + + if (!isPathWithinUploads(oldDir) || !isPathWithinUploads(newDir)) { + return res.status(400).json({ error: 'Invalid CVE ID or vendor name for file paths.' }); + } if (fs.existsSync(oldDir)) { const newParent = path.join('uploads', newCveId); @@ -428,7 +550,7 @@ app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, // Update documents table - file paths db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [existing.cve_id, existing.vendor], (docErr, docs) => { - if (docErr) return res.status(500).json({ error: docErr.message }); + if (docErr) { console.error(docErr); return res.status(500).json({ error: 'Internal server error.' }); } const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor); const newPrefix = path.join('uploads', newCveId, newVendor); @@ -469,7 +591,7 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor', // Get all rows for this CVE ID to know what we're deleting db.all('SELECT * FROM cves WHERE cve_id = ?', [cveId], (err, rows) => { - if (err) return res.status(500).json({ error: err.message }); + if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' }); // Delete all documents from DB @@ -478,11 +600,12 @@ app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor', // Delete all CVE rows db.run('DELETE FROM cves WHERE cve_id = ?', [cveId], function(cveErr) { - if (cveErr) return res.status(500).json({ error: cveErr.message }); + if (cveErr) { console.error(cveErr); return res.status(500).json({ error: 'Internal server error.' }); } - // Remove upload directory - const cveDir = path.join('uploads', cveId); - if (fs.existsSync(cveDir)) { + // Remove upload directory (with path traversal prevention) + const safeCveId = sanitizePathSegment(cveId); + const cveDir = path.join('uploads', safeCveId); + if (safeCveId && isPathWithinUploads(cveDir) && fs.existsSync(cveDir)) { fs.rmSync(cveDir, { recursive: true, force: true }); } @@ -507,17 +630,17 @@ app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (re const { id } = req.params; db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => { - if (err) return res.status(500).json({ error: err.message }); + if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!cve) return res.status(404).json({ error: 'CVE entry not found' }); // Delete associated documents from DB db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => { if (docErr) console.error('Error fetching documents:', docErr); - // Delete document files from disk + // Delete document files from disk (with path traversal prevention) if (docs && docs.length > 0) { docs.forEach(doc => { - if (doc.file_path && fs.existsSync(doc.file_path)) { + if (doc.file_path && isPathWithinUploads(doc.file_path) && fs.existsSync(doc.file_path)) { fs.unlinkSync(doc.file_path); } }); @@ -529,17 +652,17 @@ app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (re // Delete CVE row db.run('DELETE FROM cves WHERE id = ?', [id], function(delErr) { - if (delErr) return res.status(500).json({ error: delErr.message }); + if (delErr) { console.error(delErr); return res.status(500).json({ error: 'Internal server error.' }); } - // Clean up directories - const vendorDir = path.join('uploads', cve.cve_id, cve.vendor); - if (fs.existsSync(vendorDir)) { - fs.rmSync(vendorDir, { recursive: true, force: true }); + // Clean up directories (with path traversal prevention) + const safeVendorDir = path.join('uploads', sanitizePathSegment(cve.cve_id), sanitizePathSegment(cve.vendor)); + if (isPathWithinUploads(safeVendorDir) && fs.existsSync(safeVendorDir)) { + fs.rmSync(safeVendorDir, { recursive: true, force: true }); } - const cveDir = path.join('uploads', cve.cve_id); - if (fs.existsSync(cveDir)) { - const remaining = fs.readdirSync(cveDir); - if (remaining.length === 0) fs.rmdirSync(cveDir); + const safeCveDir = path.join('uploads', sanitizePathSegment(cve.cve_id)); + if (isPathWithinUploads(safeCveDir) && fs.existsSync(safeCveDir)) { + const remaining = fs.readdirSync(safeCveDir); + if (remaining.length === 0) fs.rmdirSync(safeCveDir); } logAudit(db, { @@ -578,7 +701,7 @@ app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => { db.all(query, params, (err, rows) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows); }); @@ -588,16 +711,17 @@ app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => { app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { - console.error('MULTER ERROR:', err); - return res.status(500).json({ error: 'File upload failed: ' + err.message }); + console.error('Upload error:', err.message); + // Show file validation errors to the user; hide other internal errors + if (err.message && (err.message.startsWith('File type') || err.message.startsWith('MIME type'))) { + return res.status(400).json({ error: err.message }); + } + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ error: 'File exceeds the 10MB size limit.' }); + } + return res.status(500).json({ error: 'File upload failed.' }); } - console.log('=== UPLOAD REQUEST RECEIVED ==='); - console.log('CVE ID:', req.params.cveId); - console.log('Body:', req.body); - console.log('File:', req.file); - console.log('================================'); - const { cveId } = req.params; const { type, notes, vendor } = req.body; const file = req.file; @@ -608,18 +732,41 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a } if (!vendor) { - console.error('ERROR: Vendor is required'); + // Clean up temp file + if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); return res.status(400).json({ error: 'Vendor is required' }); } + // Validate document type + if (type && !VALID_DOC_TYPES.includes(type)) { + if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); + return res.status(400).json({ error: `Invalid document type. Must be one of: ${VALID_DOC_TYPES.join(', ')}` }); + } + + // Sanitize path segments to prevent directory traversal + const safeCveId = sanitizePathSegment(cveId); + const safeVendor = sanitizePathSegment(vendor); + const safeFilename = sanitizePathSegment(file.filename); + + if (!safeCveId || !safeVendor || !safeFilename) { + if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); + return res.status(400).json({ error: 'Invalid CVE ID, vendor, or filename.' }); + } + // Move file from temp to proper location - const finalDir = path.join('uploads', cveId, vendor); + const finalDir = path.join('uploads', safeCveId, safeVendor); + const finalPath = path.join(finalDir, safeFilename); + + // Verify paths stay within uploads directory + if (!isPathWithinUploads(finalDir) || !isPathWithinUploads(finalPath)) { + if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path); + return res.status(400).json({ error: 'Invalid file path.' }); + } + if (!fs.existsSync(finalDir)) { fs.mkdirSync(finalDir, { recursive: true }); } - const finalPath = path.join(finalDir, file.filename); - // Move file from temp to final location fs.renameSync(file.path, finalPath); @@ -641,12 +788,12 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a notes ], function(err) { if (err) { - console.error('DATABASE ERROR:', err); + console.error('Document insert error:', err); // If database insert fails, delete the file if (fs.existsSync(finalPath)) { fs.unlinkSync(finalPath); } - return res.status(500).json({ error: err.message }); + return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, @@ -662,7 +809,6 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a message: 'Document uploaded successfully', file: { name: file.originalname, - path: finalPath, size: fileSizeKB } }); @@ -672,20 +818,21 @@ app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'a // Delete document (admin only) app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => { const { id } = req.params; - + // First get the file path to delete the actual file db.get('SELECT file_path FROM documents WHERE id = ?', [id], (err, row) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } - - if (row && fs.existsSync(row.file_path)) { + + // Only delete file if path is within uploads directory + if (row && row.file_path && isPathWithinUploads(row.file_path) && fs.existsSync(row.file_path)) { fs.unlinkSync(row.file_path); } db.run('DELETE FROM documents WHERE id = ?', [id], function(err) { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } logAudit(db, { userId: req.user.id, @@ -709,7 +856,7 @@ app.get('/api/vendors', requireAuth(db), (req, res) => { db.all(query, [], (err, rows) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(rows.map(r => r.vendor)); }); @@ -731,7 +878,7 @@ app.get('/api/stats', requireAuth(db), (req, res) => { db.get(query, [], (err, row) => { if (err) { - return res.status(500).json({ error: err.message }); + console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } res.json(row); }); diff --git a/frontend/src/App.js b/frontend/src/App.js index f12a882..254e14c 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -221,7 +221,7 @@ export default function App() { const handleFileUpload = async (cveId, vendor) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; - fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx'; + fileInput.accept = '.pdf,.png,.jpg,.jpeg,.gif,.bmp,.tiff,.tif,.txt,.csv,.log,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp,.rtf,.html,.htm,.xml,.json,.yaml,.yml,.zip,.gz,.tar,.7z'; fileInput.onchange = async (e) => { const file = e.target.files[0];