Skip to content

Commit 760ffda

Browse files
authored
Merge pull request #39 from atomantic/better/security
security: harden server bindings, auth gates, and error handling
2 parents 8523fcc + f400c04 commit 760ffda

9 files changed

Lines changed: 39 additions & 9 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sparsetree",
3-
"version": "0.8.4",
3+
"version": "0.8.5",
44
"private": true,
55
"description": "",
66
"main": "index.js",

server/src/db/sqlite.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ function initDb(): Database.Database {
2323
}
2424

2525
// Create database connection
26+
const isNew = !fs.existsSync(DB_PATH);
2627
db = new Database(DB_PATH, {
2728
verbose: process.env.SQLITE_VERBOSE ? console.log : undefined,
2829
});
30+
if (isNew) {
31+
fs.chmodSync(DB_PATH, 0o600);
32+
}
2933

3034
// Performance optimizations
3135
db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency

server/src/index.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import { logger } from './lib/logger.js';
3535

3636
const CORS_ORIGIN = process.env.CORS_ORIGIN || 'http://localhost:6373';
3737
const corsOrigin = CORS_ORIGIN.includes(',')
38-
? CORS_ORIGIN.split(',').map(o => o.trim())
38+
? CORS_ORIGIN.split(',').map(o => {
39+
const trimmed = o.trim();
40+
new URL(trimmed); // throws on invalid origin
41+
return trimmed;
42+
})
3943
: CORS_ORIGIN;
4044

4145
const app = express();
@@ -114,8 +118,18 @@ if (existsSync(clientDist)) {
114118
// Error handling
115119
app.use(errorHandler);
116120

117-
httpServer.listen(PORT, '0.0.0.0', () => {
118-
logger.start('server', `Running on http://localhost:${PORT}`);
121+
const HOST = process.env.HOST || 'localhost';
122+
123+
const shutdown = () => {
124+
logger.warn('server', 'Shutting down gracefully...');
125+
httpServer.close(() => process.exit(0));
126+
setTimeout(() => process.exit(1), 5000);
127+
};
128+
process.on('SIGTERM', shutdown);
129+
process.on('SIGINT', shutdown);
130+
131+
httpServer.listen(PORT, HOST, () => {
132+
logger.start('server', `Running on http://${HOST}:${PORT}`);
119133

120134
// Auto-connect to browser if enabled and browser is running
121135
browserService.autoConnectIfEnabled();

server/src/middleware/errorHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const errorHandler = (
77
res: Response,
88
_next: NextFunction
99
) => {
10-
logger.error('server', `Unhandled: ${err.stack || err.message}`);
10+
logger.error('server', `Unhandled: ${process.env.NODE_ENV !== 'production' ? (err.stack || err.message) : err.message}`);
1111
res.status(500).json({
1212
success: false,
1313
error: err.message || 'Internal server error'

server/src/routes/ancestry-update.routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ router.get('/:dbId/events', async (req: Request, res: Response) => {
5454
}
5555
}
5656

57-
const isTestMode = testMode === 'true';
57+
const isTestMode = testMode === 'true' && process.env.NODE_ENV !== 'production';
5858

5959
initSSE(res);
6060

server/src/routes/browser.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ router.get('/photos/:personId/exists', async (req: Request, res: Response) => {
210210
});
211211

212212
// Get FamilySearch authentication token from browser session
213+
// Security note: This endpoint returns an auth token in the JSON response.
214+
// Acceptable because SparseTree is a local-only tool and FS tokens are short-lived.
213215
router.get('/token', async (_req: Request, res: Response) => {
214216
if (!browserService.isConnected()) {
215217
res.status(400).json({ success: false, error: 'Browser not connected' });

server/src/routes/genealogy-provider.routes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Router, Request, Response } from 'express';
2+
import crypto from 'crypto';
23
import type { GenealogyProviderConfig, PlatformType } from '@fsf/shared';
34
import { genealogyProviderService } from '../services/genealogy-provider.service.js';
45
import { pickFields } from '../utils/validation.js';
@@ -60,7 +61,7 @@ router.post('/', (req: Request, res: Response) => {
6061

6162
// Generate ID if not provided
6263
if (!config.id) {
63-
config.id = config.platform + '-' + Date.now();
64+
config.id = config.platform + '-' + crypto.randomUUID();
6465
}
6566

6667
// Set defaults if not provided

server/src/routes/test-runner.routes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import { logger } from '../lib/logger.js';
44

55
export const testRunnerRouter = Router();
66

7+
// Gate all test-runner endpoints behind non-production environment
8+
testRunnerRouter.use((_req, res, next) => {
9+
if (process.env.NODE_ENV === 'production') {
10+
res.status(403).json({ success: false, error: 'Test runner is disabled in production' });
11+
return;
12+
}
13+
next();
14+
});
15+
716
// GET /api/test-runner/status - Get current test run status
817
testRunnerRouter.get('/status', (_req, res) => {
918
res.json({ success: true, data: testRunnerService.getStatus() });

0 commit comments

Comments
 (0)