From dfdd473eca6f60b9ef4ef0f9e2836fc10e5a4e00 Mon Sep 17 00:00:00 2001 From: jubnl Date: Wed, 1 Apr 2026 05:54:03 +0200 Subject: [PATCH] fix: validate uploaded backup DB before restore Before swapping in a restored database, run PRAGMA integrity_check and verify the five core TREK tables (users, trips, trip_members, places, days) are present. This blocks restoring corrupt, empty, or unrelated SQLite files that would otherwise crash the app immediately after swap, and prevents a malicious admin from hot-swapping a crafted database with forged users or permissions. --- server/src/routes/backup.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/server/src/routes/backup.ts b/server/src/routes/backup.ts index de47375..53a772c 100644 --- a/server/src/routes/backup.ts +++ b/server/src/routes/backup.ts @@ -4,6 +4,7 @@ import unzipper from 'unzipper'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; +import Database from 'better-sqlite3'; import { authenticate, adminOnly } from '../middleware/auth'; import * as scheduler from '../scheduler'; import { db, closeDb, reinitialize } from '../db/database'; @@ -159,6 +160,34 @@ async function restoreFromZip(zipPath: string, res: Response, audit?: RestoreAud return res.status(400).json({ error: 'Invalid backup: travel.db not found' }); } + let uploadedDb: InstanceType | null = null; + try { + uploadedDb = new Database(extractedDb, { readonly: true }); + + const integrityResult = uploadedDb.prepare('PRAGMA integrity_check').get() as { integrity_check: string }; + if (integrityResult.integrity_check !== 'ok') { + fs.rmSync(extractDir, { recursive: true, force: true }); + return res.status(400).json({ error: `Uploaded database failed integrity check: ${integrityResult.integrity_check}` }); + } + + const requiredTables = ['users', 'trips', 'trip_members', 'places', 'days']; + const existingTables = uploadedDb + .prepare("SELECT name FROM sqlite_master WHERE type='table'") + .all() as { name: string }[]; + const tableNames = new Set(existingTables.map(t => t.name)); + for (const table of requiredTables) { + if (!tableNames.has(table)) { + fs.rmSync(extractDir, { recursive: true, force: true }); + return res.status(400).json({ error: `Uploaded database is missing required table: ${table}. This does not appear to be a TREK backup.` }); + } + } + } catch (err) { + fs.rmSync(extractDir, { recursive: true, force: true }); + return res.status(400).json({ error: 'Uploaded file is not a valid SQLite database' }); + } finally { + uploadedDb?.close(); + } + closeDb(); try {