From 5e517c27012985e91d76d55e4a5d5edfbd6c54ef Mon Sep 17 00:00:00 2001 From: Adam Eivy Date: Thu, 5 Mar 2026 18:25:46 -0800 Subject: [PATCH 1/2] security: harden server bindings, auth gates, and error handling --- server/src/db/sqlite.service.ts | 4 ++++ server/src/index.ts | 20 ++++++++++++++++--- server/src/middleware/errorHandler.ts | 2 +- server/src/routes/ancestry-update.routes.ts | 2 +- server/src/routes/browser.routes.ts | 2 ++ .../src/routes/genealogy-provider.routes.ts | 3 ++- server/src/routes/test-runner.routes.ts | 9 +++++++++ 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/server/src/db/sqlite.service.ts b/server/src/db/sqlite.service.ts index 7d43ef35..a380b763 100644 --- a/server/src/db/sqlite.service.ts +++ b/server/src/db/sqlite.service.ts @@ -23,9 +23,13 @@ function initDb(): Database.Database { } // Create database connection + const isNew = !fs.existsSync(DB_PATH); db = new Database(DB_PATH, { verbose: process.env.SQLITE_VERBOSE ? console.log : undefined, }); + if (isNew) { + fs.chmodSync(DB_PATH, 0o600); + } // Performance optimizations db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency diff --git a/server/src/index.ts b/server/src/index.ts index 89f06799..602ca80a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -35,7 +35,11 @@ import { logger } from './lib/logger.js'; const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:6373'; const corsOrigin = CORS_ORIGIN.includes(',') - ? CORS_ORIGIN.split(',').map(o => o.trim()) + ? CORS_ORIGIN.split(',').map(o => { + const trimmed = o.trim(); + new URL(trimmed); // throws on invalid origin + return trimmed; + }) : CORS_ORIGIN; const app = express(); @@ -114,8 +118,18 @@ if (existsSync(clientDist)) { // Error handling app.use(errorHandler); -httpServer.listen(PORT, '0.0.0.0', () => { - logger.start('server', `Running on http://localhost:${PORT}`); +const HOST = process.env.HOST || 'localhost'; + +const shutdown = () => { + logger.warn('server', 'Shutting down gracefully...'); + httpServer.close(() => process.exit(0)); + setTimeout(() => process.exit(1), 5000); +}; +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); + +httpServer.listen(PORT, HOST, () => { + logger.start('server', `Running on http://${HOST}:${PORT}`); // Auto-connect to browser if enabled and browser is running browserService.autoConnectIfEnabled(); diff --git a/server/src/middleware/errorHandler.ts b/server/src/middleware/errorHandler.ts index 071ac028..dd3f5a9b 100644 --- a/server/src/middleware/errorHandler.ts +++ b/server/src/middleware/errorHandler.ts @@ -7,7 +7,7 @@ export const errorHandler = ( res: Response, _next: NextFunction ) => { - logger.error('server', `Unhandled: ${err.stack || err.message}`); + logger.error('server', `Unhandled: ${process.env.NODE_ENV !== 'production' ? (err.stack || err.message) : err.message}`); res.status(500).json({ success: false, error: err.message || 'Internal server error' diff --git a/server/src/routes/ancestry-update.routes.ts b/server/src/routes/ancestry-update.routes.ts index 8790a16d..715cc7f6 100644 --- a/server/src/routes/ancestry-update.routes.ts +++ b/server/src/routes/ancestry-update.routes.ts @@ -54,7 +54,7 @@ router.get('/:dbId/events', async (req: Request, res: Response) => { } } - const isTestMode = testMode === 'true'; + const isTestMode = testMode === 'true' && process.env.NODE_ENV !== 'production'; initSSE(res); diff --git a/server/src/routes/browser.routes.ts b/server/src/routes/browser.routes.ts index 5dfa85f1..ea817ac3 100644 --- a/server/src/routes/browser.routes.ts +++ b/server/src/routes/browser.routes.ts @@ -210,6 +210,8 @@ router.get('/photos/:personId/exists', async (req: Request, res: Response) => { }); // Get FamilySearch authentication token from browser session +// Security note: This endpoint returns an auth token in the JSON response. +// Acceptable because SparseTree is a local-only tool and FS tokens are short-lived. router.get('/token', async (_req: Request, res: Response) => { if (!browserService.isConnected()) { res.status(400).json({ success: false, error: 'Browser not connected' }); diff --git a/server/src/routes/genealogy-provider.routes.ts b/server/src/routes/genealogy-provider.routes.ts index 702ef60b..b7e2d9c4 100644 --- a/server/src/routes/genealogy-provider.routes.ts +++ b/server/src/routes/genealogy-provider.routes.ts @@ -1,4 +1,5 @@ import { Router, Request, Response } from 'express'; +import crypto from 'crypto'; import type { GenealogyProviderConfig, PlatformType } from '@fsf/shared'; import { genealogyProviderService } from '../services/genealogy-provider.service.js'; import { pickFields } from '../utils/validation.js'; @@ -60,7 +61,7 @@ router.post('/', (req: Request, res: Response) => { // Generate ID if not provided if (!config.id) { - config.id = config.platform + '-' + Date.now(); + config.id = config.platform + '-' + crypto.randomUUID(); } // Set defaults if not provided diff --git a/server/src/routes/test-runner.routes.ts b/server/src/routes/test-runner.routes.ts index a240d0fd..ddf0af37 100644 --- a/server/src/routes/test-runner.routes.ts +++ b/server/src/routes/test-runner.routes.ts @@ -4,6 +4,15 @@ import { logger } from '../lib/logger.js'; export const testRunnerRouter = Router(); +// Gate all test-runner endpoints behind non-production environment +testRunnerRouter.use((_req, res, next) => { + if (process.env.NODE_ENV === 'production') { + res.status(403).json({ success: false, error: 'Test runner is disabled in production' }); + return; + } + next(); +}); + // GET /api/test-runner/status - Get current test run status testRunnerRouter.get('/status', (_req, res) => { res.json({ success: true, data: testRunnerService.getStatus() }); From f400c04676b3cfb16232a90cdc223181e9372e71 Mon Sep 17 00:00:00 2001 From: Adam Eivy Date: Thu, 5 Mar 2026 18:25:59 -0800 Subject: [PATCH 2/2] chore: bump version to 0.8.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e0da9b4..62528d94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sparsetree", - "version": "0.8.4", + "version": "0.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sparsetree", - "version": "0.8.4", + "version": "0.8.5", "license": "ISC", "workspaces": [ "shared", diff --git a/package.json b/package.json index c7448a6b..b375cf2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sparsetree", - "version": "0.8.4", + "version": "0.8.5", "private": true, "description": "", "main": "index.js",