Compare commits

..

1 Commits

Author SHA1 Message Date
Zimeng Xiong 6fe136ae5a validate SQlite magic header 2025-11-22 21:27:34 -08:00
2 changed files with 55 additions and 44 deletions
+55 -26
View File
@@ -5,7 +5,6 @@ import path from "path";
import fs from "fs"; import fs from "fs";
import { createServer } from "http"; import { createServer } from "http";
import { Server } from "socket.io"; import { Server } from "socket.io";
import { Worker } from "worker_threads";
import multer from "multer"; import multer from "multer";
import archiver from "archiver"; import archiver from "archiver";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
@@ -126,31 +125,61 @@ const respondWithValidationErrors = (
}); });
}; };
// Non-blocking CPU check using worker threads const validateSqliteHeader = (filePath: string): boolean => {
const verifyDatabaseIntegrityAsync = (filePath: string): Promise<boolean> => { try {
return new Promise((resolve) => { const buffer = Buffer.alloc(16);
const worker = new Worker( const fd = fs.openSync(filePath, "r");
path.resolve(__dirname, "./workers/db-verify.js"), const bytesRead = fs.readSync(fd, buffer, 0, 16, 0);
{ fs.closeSync(fd);
workerData: { filePath },
}
);
worker.on("message", (isValid: boolean) => resolve(isValid)); if (bytesRead < 16) {
worker.on("error", (err) => { console.warn("File too small to be a valid SQLite database");
console.error("Worker error:", err); return false;
resolve(false); }
});
worker.on("exit", (code) => {
if (code !== 0) resolve(false);
});
// Kill worker if it takes too long (DoS protection) // SQLite format 3 header: "SQLite format 3\0" (16 bytes)
setTimeout(() => { // Hex: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
worker.terminate(); const expectedHeader = Buffer.from([
resolve(false); 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61,
}, 10000); // 10 second timeout 0x74, 0x20, 0x33, 0x00,
}); ]);
const isValid = buffer.equals(expectedHeader);
if (!isValid) {
console.warn("Invalid SQLite file header detected", {
filePath,
header: buffer.toString("hex"),
expected: expectedHeader.toString("hex"),
});
}
return isValid;
} catch (error) {
console.error("Failed to validate SQLite header:", error);
return false;
}
};
const runIntegrityCheck = (filePath: string): boolean => {
// First validate the file header to prevent RCE attacks
if (!validateSqliteHeader(filePath)) {
return false;
}
let dbInstance: Database.Database | undefined;
try {
dbInstance = new Database(filePath, {
readonly: true,
fileMustExist: true,
});
const result = dbInstance.prepare("PRAGMA integrity_check;").get();
return result?.integrity_check === "ok";
} catch (error) {
console.error("Integrity check failed:", error);
return false;
} finally {
dbInstance?.close();
}
}; };
const removeFileIfExists = (filePath?: string) => { const removeFileIfExists = (filePath?: string) => {
@@ -656,7 +685,7 @@ app.post("/import/sqlite/verify", upload.single("db"), async (req, res) => {
} }
const stagedPath = req.file.path; const stagedPath = req.file.path;
const isValid = await verifyDatabaseIntegrityAsync(stagedPath); const isValid = runIntegrityCheck(stagedPath);
removeFileIfExists(stagedPath); removeFileIfExists(stagedPath);
if (!isValid) { if (!isValid) {
@@ -695,7 +724,7 @@ app.post("/import/sqlite", upload.single("db"), async (req, res) => {
return res.status(500).json({ error: "Failed to stage uploaded file" }); return res.status(500).json({ error: "Failed to stage uploaded file" });
} }
const isValid = await verifyDatabaseIntegrityAsync(stagedPath); const isValid = runIntegrityCheck(stagedPath);
if (!isValid) { if (!isValid) {
removeFileIfExists(stagedPath); removeFileIfExists(stagedPath);
return res return res
-18
View File
@@ -1,18 +0,0 @@
const { parentPort, workerData } = require('worker_threads');
const Database = require('better-sqlite3');
if (!parentPort) throw new Error("Must be run in a worker thread");
try {
const { filePath } = workerData;
const db = new Database(filePath, { readonly: true, fileMustExist: true });
// This is the CPU-heavy operation
const result = db.prepare("PRAGMA integrity_check;").get();
db.close();
parentPort.postMessage(result.integrity_check === "ok");
} catch (error) {
// Any error means invalid or corrupt DB
parentPort.postMessage(false);
}