Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fe136ae5a |
+53
-24
@@ -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 },
|
|
||||||
|
if (bytesRead < 16) {
|
||||||
|
console.warn("File too small to be a valid SQLite database");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
worker.on("message", (isValid: boolean) => resolve(isValid));
|
// SQLite format 3 header: "SQLite format 3\0" (16 bytes)
|
||||||
worker.on("error", (err) => {
|
// Hex: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
|
||||||
console.error("Worker error:", err);
|
const expectedHeader = Buffer.from([
|
||||||
resolve(false);
|
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61,
|
||||||
});
|
0x74, 0x20, 0x33, 0x00,
|
||||||
worker.on("exit", (code) => {
|
]);
|
||||||
if (code !== 0) resolve(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Kill worker if it takes too long (DoS protection)
|
const isValid = buffer.equals(expectedHeader);
|
||||||
setTimeout(() => {
|
if (!isValid) {
|
||||||
worker.terminate();
|
console.warn("Invalid SQLite file header detected", {
|
||||||
resolve(false);
|
filePath,
|
||||||
}, 10000); // 10 second timeout
|
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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user